Skip to content

Commit a439428

Browse files
committed
fix(react): fix stale SSR state during cross-tab sign-out
1 parent 43e4972 commit a439428

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

integration/tests/transitions.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,44 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('transitio
189189
.expect(u.po.page.getByTestId('fetcher-result'))
190190
.toHaveText(`Fetched value: ${fakeOrganization.organization.id}`);
191191
});
192+
193+
/*
194+
This test verifies that when a user signs out in one browser tab, other tabs detect the sign-out
195+
and redirect to the sign-in page without hanging. This tests the fix for the cross-tab sign-out bug
196+
where useAuthBase would return stale initialState instead of undefined during the sign-out transition.
197+
198+
The expected behavior is:
199+
1. Tab 1 and Tab 2 are both authenticated and viewing a protected page
200+
2. User signs out in Tab 2
201+
3. Tab 1 should detect the sign-out via BroadcastChannel
202+
4. Tab 1 should redirect to sign-in (not hang with stale session data)
203+
*/
204+
test('should redirect background tabs to sign-in when user signs out in another tab', async ({ page, context }) => {
205+
const u1 = createTestUtils({ app, page, context });
206+
207+
await u1.po.signIn.goTo();
208+
await u1.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
209+
await u1.po.expect.toBeSignedIn();
210+
211+
await u1.po.page.goToRelative('/protected');
212+
await u1.po.expect.toBeSignedIn();
213+
214+
const page2 = await context.newPage();
215+
const u2 = createTestUtils({ app, page: page2, context });
216+
217+
await u2.po.page.goToRelative('/');
218+
await u2.po.expect.toBeSignedIn();
219+
220+
await u2.po.userButton.waitForMounted();
221+
await u2.po.userButton.toggleTrigger();
222+
await u2.po.userButton.waitForPopover();
223+
await u2.po.userButton.triggerSignOut();
224+
225+
await u2.po.expect.toBeSignedOut();
226+
227+
await test.expect(page).toHaveURL(/.*sign-in.*/);
228+
await u1.po.expect.toBeSignedOut();
229+
230+
await page2.close();
231+
});
192232
});

packages/react/src/hooks/useAuthBase.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,20 @@ export function useAuthBase(): AuthStateValue {
4646
const state = useSyncExternalStore(
4747
useCallback(callback => clerk.addListener(callback, { skipInitialEmit: true }), [clerk]),
4848
useCallback(() => {
49-
if (!clerk.loaded || !clerk.__internal_lastEmittedResources) {
49+
const loaded = clerk.loaded;
50+
const hasResources = !!clerk.__internal_lastEmittedResources;
51+
52+
if (!loaded && !hasResources) {
5053
return getInitialState();
5154
}
5255

53-
return clerk.__internal_lastEmittedResources;
56+
// Once clerk is loaded or we have resources, never fall back to initialState
57+
// because initialState is a frozen SSR snapshot that can contain stale auth data
58+
if (hasResources) {
59+
return clerk.__internal_lastEmittedResources;
60+
}
61+
62+
return undefined;
5463
}, [clerk, getInitialState]),
5564
getInitialState,
5665
);

0 commit comments

Comments
 (0)