Replies: 1 comment
-
Here is a solution that has worked nicely in practice for me. Though it requires some complicated setup code to be written once up front, no changes are required to any existing authentication provider code and using the authentication context in Suppose we have some authentication state stored in a context Now, how do we write Disclaimer: I'm writing this code from memory. There may be typos. However, I have used the same principle in a private project which has been tested more extensively, so the idea is sound. I recommend reading the code, understanding the idea it uses, and implementing it yourself again in your project if it makes sense. interface LoadingAuth {
resolve: (auth: AuthContext) => void;
promise: Promise<AuthContext>;
}
function App() {
const auth = useAuth();
const loadingAuthRef = useRef<LoadingAuth | undefined>(undefined);
if (!loadingAuthRef.current) {
// Initial render of the app; initialize `loadingAuthRef`.
// In sufficiently modern environments one can use `Promise.withResolvers` to create the deferred promise here.
let resolve!: (auth: AuthContext) => void;
const promise = new Promise<AuthContext>((resolve_) => {
resolve = resolve_;
});
loadingAuthRef.current = { resolve, promise };
}
useEffect(() => {
if (auth.isLoading) return;
// Finished loading; propagate the loaded authentication context to clients waiting on the promise.
loadingAuthRef.current!.resolve(auth);
}, [auth.isLoading]);
const loadAuth = async () => {
// SUBTLY BUGGY; see diff below for the fix.
return loadingAuthRef.current!.promise;
};
return <RouterProvider router={router} context={{ loadAuth }}>{...}</RouterProvider>;
} The above code resolves the initial problem: downstream routes can just write async beforeLoad({ context }) {
const auth = await context.loadAuth();
if (!auth.session) {
// ...redirect...
}
// ...
} However, it runs into a subtle problem when the authentication context is updated after the initial load (e.g., if a logout is issued.) In this case, the authentication context returned by The fix is to adjust const loadAuth = async () => {
- return loadingAuthRef.current!.promise;
+ if (auth.isLoading) return loadingAuthRef.current!.promise;
+ return auth;
} (Rest of the code remains the same.) This final solution ends up working nicely in all scenarios I have tested, and although the implementation of |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
TL;DR
Change
isAuthenticated
in the docs fromboolean
toboolean | undefined
or something else that reflects a loading state. Adjust code to be appropriate.The only way I've found to make it work is through a custom
useUser() => User
hook but I'd love to know if there's a correct way to do it with Router context. Unfortunately the documentation example breaks pretty badly when trying this.Explanation of the Problem
The docs all treat authentication as a boolean state.
Unfortunately in client side code we have also have to deal with
isAuthenticated: undefined
while we asynchronously load the authentication from a server.eg. from the Clerk types:
This causes a significant issue with the recommended auth pattern of using
beforeLoad
.This will cause the user to be redirected to the sign in page while the auth is still loading. I'd consider this inappropriate to do unless the user is known to be signed out, but we also don't want to let them proceed to the authenticated page.
They then hit the
beforeLoad
onsign-in.tsx
The auth will still be loading, so the
throw redirect
is not triggered.Shortly after the auth will be loaded.
This can lead to a signed-in user sitting on the loaded
/sign-in
page. They've already passed thebeforeLoad
, so there's nothing to trigger theredirect
. It's possible to hack around it withuseEffect
or something similar but it's ugly.Evidence of Need
(and probably more but I think the point is made)
Beta Was this translation helpful? Give feedback.
All reactions