Skip to content

Commit 816cd12

Browse files
committed
fix(nextjs,clerk-js): Make useAwaitableNavigate handle navigations between pages reliably
We need to use window to store the reference to the buffer, as ClerkProvider might be unmounted and remounted during navigations. If we use a ref, it will be reset when ClerkProvider is unmounted
1 parent 9dc46b2 commit 816cd12

6 files changed

Lines changed: 87 additions & 33 deletions

File tree

‎.changeset/silly-tips-cry.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/nextjs': patch
4+
---
5+
6+
Make useAwaitableNavigate handle navigations between pages reliably

‎packages/clerk-js/src/core/clerk.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ export class Clerk implements ClerkInterface {
618618
);
619619
}
620620

621-
type SetActiveHook = () => void;
621+
type SetActiveHook = () => void | Promise<void>;
622622
const onBeforeSetActive: SetActiveHook =
623623
typeof window !== 'undefined' && typeof window.__unstable__onBeforeSetActive === 'function'
624624
? window.__unstable__onBeforeSetActive
@@ -653,7 +653,7 @@ export class Clerk implements ClerkInterface {
653653
this.#broadcastSignOutEvent();
654654
}
655655

656-
onBeforeSetActive();
656+
await onBeforeSetActive();
657657

658658
//1. setLastActiveSession to passed user session (add a param).
659659
// Note that this will also update the session's active organization
@@ -685,7 +685,7 @@ export class Clerk implements ClerkInterface {
685685

686686
this.#setAccessors(newSession);
687687
this.#emit();
688-
onAfterSetActive();
688+
await onAfterSetActive();
689689
this.#resetComponentsState();
690690
};
691691

‎packages/clerk-js/src/globals.d.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ declare global {
44
const __PKG_VERSION__: string;
55

66
interface Window {
7-
__unstable__onBeforeSetActive: () => void;
8-
__unstable__onAfterSetActive: () => void;
7+
__unstable__onBeforeSetActive: () => Promise<void> | void;
8+
__unstable__onAfterSetActive: () => Promise<void> | void;
99
}
1010
}
1111

‎packages/nextjs/src/app-router/client/ClerkProvider.tsx‎

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22
import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react';
33
import { useRouter } from 'next/navigation';
4-
import React from 'react';
4+
import React, { useEffect, useTransition } from 'react';
55

66
import { ClerkNextOptionsProvider } from '../../client-boundary/NextOptionsContext';
77
import { useSafeLayoutEffect } from '../../client-boundary/useSafeLayoutEffect';
@@ -13,24 +13,55 @@ declare global {
1313
export interface Window {
1414
__clerk_nav_await: Array<(value: void) => void>;
1515
__clerk_nav: (to: string) => Promise<void>;
16+
__clerk_internal_invalidateCachePromise: () => void | undefined;
1617
}
1718
}
1819

1920
export const ClientClerkProvider = (props: NextClerkProviderProps) => {
2021
const { __unstable_invokeMiddlewareOnAuthStateChange = true, children } = props;
2122
const router = useRouter();
2223
const navigate = useAwaitableNavigate();
24+
const [isPending, startTransition] = useTransition();
25+
26+
useEffect(() => {
27+
if (!isPending) {
28+
window.__clerk_internal_invalidateCachePromise?.();
29+
}
30+
}, [isPending]);
2331

2432
useSafeLayoutEffect(() => {
2533
window.__unstable__onBeforeSetActive = () => {
26-
if (__unstable_invokeMiddlewareOnAuthStateChange) {
27-
router.refresh();
28-
router.push(window.location.href);
29-
}
34+
/**
35+
* We need to invalidate the cache in case the user is navigating to a page that
36+
* was previously cached using the auth state that was active at the time.
37+
*
38+
* We also need to await for the invalidation to happen before we navigate,
39+
* otherwise the navigation will use the cached page.
40+
*
41+
* For example, if we did not invalidate the flow, the following scenario would be broken:
42+
* - The middleware is configured in such a way that it redirects you back to the same page if a certain condition is true (eg, you need to pick an org)
43+
* - The user has a <Link href=/> component in the page
44+
* - The UB is mounted with afterSignOutUrl=/
45+
* - The user clicks the Link. A nav to / happens, a 307 to the current page is returned so a navigation does not take place. The / navigation is now cached as a 307 to the current page
46+
* - The user clicks sign out
47+
* - We call router.refresh()
48+
* - We navigate to / but its cached and instead, we 'redirect' to the current page
49+
*
50+
* For more information on cache invalidation, see:
51+
* https://nextjs.org/docs/app/building-your-application/caching#invalidation-1
52+
*/
53+
return new Promise(res => {
54+
window.__clerk_internal_invalidateCachePromise = res;
55+
startTransition(() => {
56+
router.refresh();
57+
});
58+
});
3059
};
3160

3261
window.__unstable__onAfterSetActive = () => {
33-
router.refresh();
62+
if (__unstable_invokeMiddlewareOnAuthStateChange) {
63+
return router.refresh();
64+
}
3465
};
3566
}, []);
3667

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
'use client';
22

3-
import { useRouter } from 'next/navigation';
4-
import { useEffect, useRef, useTransition } from 'react';
3+
import { usePathname, useRouter } from 'next/navigation';
4+
import { useCallback, useEffect, useTransition } from 'react';
55

66
import type { NextClerkProviderProps } from '../../types';
77

8+
declare global {
9+
interface Window {
10+
__clerk_internal_navPromisesBuffer: Array<() => void> | undefined;
11+
__clerk_internal_navFun: NonNullable<NextClerkProviderProps['routerPush']>;
12+
}
13+
}
14+
815
/**
916
* Creates an "awaitable" navigation function that will do its best effort to wait for Next.js to finish its route transition.
10-
*
1117
* This is accomplished by wrapping the call to `router.push` in `startTransition()`, which should rely on React to coordinate the pending state. We key off of
1218
* `isPending` to flush the stored promises and ensure the navigates "resolve".
1319
*/
1420
export const useAwaitableNavigate = () => {
1521
const router = useRouter();
22+
const pathname = usePathname();
1623
const [isPending, startTransition] = useTransition();
17-
const clerkNavRef = useRef<NonNullable<NextClerkProviderProps['routerPush']>>();
18-
const clerkNavPromiseBuffer = useRef<(() => void)[]>([]);
1924

20-
// Set the navigation function reference only once
21-
if (!clerkNavRef.current) {
22-
clerkNavRef.current = ((to, opts) => {
25+
if (typeof window !== 'undefined') {
26+
window.__clerk_internal_navFun = (to, opts) => {
2327
return new Promise<void>(res => {
24-
clerkNavPromiseBuffer.current.push(res);
28+
if (!window.__clerk_internal_navPromisesBuffer) {
29+
// We need to use window to store the reference to the buffer,
30+
// as ClerkProvider might be unmounted and remounted during navigations
31+
// If we use a ref, it will be reset when ClerkProvider is unmounted
32+
window.__clerk_internal_navPromisesBuffer = [];
33+
}
34+
window.__clerk_internal_navPromisesBuffer.push(res);
2535
startTransition(() => {
2636
// If the navigation is internal, we should use the history API to navigate
2737
// as this is the way to perform a shallow navigation in Next.js App Router
@@ -41,21 +51,28 @@ export const useAwaitableNavigate = () => {
4151
}
4252
});
4353
});
44-
}) as NextClerkProviderProps['routerPush'];
54+
};
4555
}
4656

47-
// Handle flushing the promise buffer when pending is false. If pending is false and there are promises in the buffer we should be able to safely flush them.
57+
const flushPromises = () => {
58+
window.__clerk_internal_navPromisesBuffer?.forEach(resolve => resolve());
59+
window.__clerk_internal_navPromisesBuffer = [];
60+
};
61+
62+
// Flush any pending promises on mount/unmount
4863
useEffect(() => {
49-
if (isPending) {
50-
return;
51-
}
64+
flushPromises();
65+
return flushPromises;
66+
}, []);
5267

53-
if (clerkNavPromiseBuffer?.current?.length) {
54-
clerkNavPromiseBuffer.current.forEach(resolve => resolve());
68+
// Handle flushing the promise buffer when a navigation happens
69+
useEffect(() => {
70+
if (!isPending) {
71+
flushPromises();
5572
}
73+
}, [pathname, isPending]);
5674

57-
clerkNavPromiseBuffer.current = [];
58-
}, [isPending]);
59-
60-
return clerkNavRef.current;
75+
return useCallback((to: string) => {
76+
return window.__clerk_internal_navFun(to);
77+
}, []);
6178
};

‎packages/nextjs/src/global.d.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
declare global {
22
interface Window {
3-
__unstable__onBeforeSetActive: () => void;
4-
__unstable__onAfterSetActive: () => void;
3+
__unstable__onBeforeSetActive: () => void | Promise<void>;
4+
__unstable__onAfterSetActive: () => void | Promise<void>;
55
}
66
}
77

0 commit comments

Comments
 (0)