Skip to content

fix(clerk-js): Fix broken transitive state for org switch and signout#7823

Open
Ephem wants to merge 11 commits intomainfrom
fredrik/user-4666-fix-transitive-state-when-switching-orgs
Open

fix(clerk-js): Fix broken transitive state for org switch and signout#7823
Ephem wants to merge 11 commits intomainfrom
fredrik/user-4666-fix-transitive-state-when-switching-orgs

Conversation

@Ephem
Copy link
Member

@Ephem Ephem commented Feb 11, 2026

Description

This PR fixes a bug that was introduced when we migrated to useSyncExternalStore. The bug is that when switching orgs and navigating to a new page, the current page re-rendered with the new organization before the navigation, which can cause problems.

Alternative solution: #7815

Compared to that solution, this touches more code, but is scoped to only block updateClient from emitting for the specific setActive call that triggered it, it does not block all updateClient calls to keep the splash radius as small as possible.

We want to refactor this more heavily to have better guarantees, but now is not the time.

I went with a version where a __internal_touch() does not call updateClient and instead returns a client resource, expecting the caller to then call updateClient with the result. updateClient now takes an option to skip emitting.

If we want to refactor further in the future, this approach let's us move the updateClient call around, and also do things with the intermediary client if necessary.

Another version would be to thread a skipUiEmit option all the way through the base resource calls, but in total that's 6 stacks deep and couples UI concerns to resources in a way I did not like.

UPDATE

I found the same bug applied for signing out. I added two tests for this, single session and multi session.

The issue for signOut was that the session removal ran before setTransitiveState, which like the above bug triggered an updateClient which emitted.

Since this flow was simpler I opted for a simpler fix, to move the setTransitiveState to before the session removal. When updateClient is called, the if (this.session) guard detects there is no current session and skips the updateAccessors call. It still emits, but with the transitive state. Later when updateAccessors is called during the signOut, this converts the undefined to null and we are now in a correctly signed out case.

I have verified these fixes works in the dashboard.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • New Features

    • Added organization switcher, user switcher, and sign-out pages/layouts including an "Emission log" view to observe navigation/state transitions.
  • Bug Fixes

    • Improved auth state handling to defer or skip premature emissions during sign-in/out, user/org switches, and navigation for more consistent transitive state.
  • Tests

    • Added integration tests for transitive state emissions and unit tests for internal touch/update flows.
  • Chores

    • Added a changeset for a patch release and updated bundle size threshold.

@vercel
Copy link

vercel bot commented Feb 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Feb 13, 2026 2:15pm

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Feb 11, 2026

🦋 Changeset detected

Latest commit: 549a027

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@clerk/clerk-js Patch
@clerk/chrome-extension Patch
@clerk/expo Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR introduces an optional __internal_dangerouslySkipEmit flag on updateClient, a Session.__internal_touch() method that performs a server touch and can return a piggybacked ClientResource without updating the client, and a skipUpdateClient fetch option. The setActive and sign-out flows were refactored to be navigation-aware and to use the touch + updateClient (skip-emission) patterns. It also adds Next.js App Router pages and client layouts that record emission logs, integration and unit tests for transitive state and sign-out scenarios, and a changeset for a patch release.

🚥 Pre-merge checks | ✅ 4 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (12 files):

⚔️ integration/tests/sign-out-smoke.test.ts (content)
⚔️ packages/clerk-js/bundlewatch.config.json (content)
⚔️ packages/clerk-js/src/core/__tests__/clerk.test.ts (content)
⚔️ packages/clerk-js/src/core/clerk.ts (content)
⚔️ packages/clerk-js/src/core/resources/Base.ts (content)
⚔️ packages/clerk-js/src/core/resources/Client.ts (content)
⚔️ packages/clerk-js/src/core/resources/Session.ts (content)
⚔️ packages/clerk-js/src/core/resources/__tests__/Session.test.ts (content)
⚔️ packages/react-router/src/client/usePathnameWithoutSplatRouteParams.tsx (content)
⚔️ packages/tanstack-react-start/src/client/ClerkProvider.tsx (content)
⚔️ packages/tanstack-react-start/src/client/uiComponents.tsx (content)
⚔️ packages/tanstack-react-start/src/client/utils.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(clerk-js): Fix broken transitive state for org switch and signout' clearly and specifically describes the main changes: fixing transitive state for organization switching and sign-out flows in clerk-js.
Linked Issues check ✅ Passed The PR successfully addresses USER-4666 objectives by implementing __internal_touch() method, updateClient skip-emit option, adjusting setActive flow to prevent premature emissions, and fixing sign-out ordering—all ensuring correct transitive state during org switches and navigation.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing transitive state: internal touch method, skip-emit controls, setActive navigation logic, sign-out ordering, and comprehensive integration tests validating the fixes—no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


No actionable comments were generated in the recent review. 🎉

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 11, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7823

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7823

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7823

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7823

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7823

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7823

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@7823

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7823

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7823

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7823

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@7823

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7823

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7823

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7823

@clerk/react

npm i https://pkg.pr.new/@clerk/react@7823

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7823

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7823

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7823

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7823

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@7823

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7823

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7823

commit: 549a027

@Ephem Ephem changed the title fix(clerk-js): Fix broken transitive state when switching organization fix(clerk-js): Fix broken transitive state for org switch and signout Feb 12, 2026
Comment on lines +9 to +13
/*
These tests verify that useAuth emits the correct transitive state sequence when switching
auth context (org or user) with navigation. The expected pattern is:
Path A - Value A, Path A - undefined, Path B - undefined, Path B - Value B
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

Copy link
Member

@brkalow brkalow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! This is a pragmatic way to go about it. Great to have tests for this going forward.

}

protected static _updateClient<J>(responseJSON: FapiResponseJSON<J> | null): void {
protected static _getClientResourceFromPayload<J>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Consider moving this to Client, or making it a standalone helper.

Comment on lines 118 to 132
__internal_touch = async (): Promise<ClientResource | undefined> => {
const json = await BaseResource._fetch<SessionJSON>(
{
method: 'POST',
path: this.path('touch'),
body: { active_organization_id: this.lastActiveOrganizationId } as any,
},
{ skipUpdateClient: true },
);

// Update session in-place from response (same as _baseMutate)
this.fromJSON((json?.response || json) as SessionJSON);

return BaseResource._getClientResourceFromPayload(json);
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Consider consolidating the fetch across touch() and __internal_touch

// reload session from updated client
newSession = this.#getSessionFromClient(newSession?.id);
let updatedClient: ClientResource | undefined;
if (shouldNavigate && newSession && '__internal_touch' in newSession) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Curious why the duck-type check and not just a newSession typeof Session here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, I forgot to go back on this. Tests were failing because they mocked Session without the __internal_touch function and I just ran with it while testing things out. I don't like tests affecting real code though so meant to go back and implement that mock instead but never did. Will fix!

Copy link
Member

@jacekradko jacekradko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. I don't think there is anything blocking here, just some nits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants