RJ
RJ2y ago

`<Unauthenticated>` renders after sign in redirect

I'm using Clerk's <SignIn /> and <SignUp /> components on their own dedicated routes (using Remix)—/sign-in and /sign-up respectively. When a user navigates to the index route /, I use the <Unauthenticated /> component to conditionally render Clerks' <RedirectToSignIn /> component, which does what it sounds like. This all works great without issue, until the user signs in—once that happens, the user is redirected back to /, and then—apparently because there is some delay between this redirect and the <Unauthenticated /> component registering that the user is now authenticated—the <Unauthenticated /> component redirects the user to the the sign in route, which then redirects the user to the index route (because Clerk realizes they're already logged in), which then redirects the user to the sign in route, etc. I was able to break this loop by replacing
<Unauthenticated>
<RedirectToSignIn />
</Unauthenticated>
<Unauthenticated>
<RedirectToSignIn />
</Unauthenticated>
with
<Unauthenticated>
<UnauthenticatedApp />
</Unauthenticated>

...

const UnauthenticatedApp = () => {
const [delayElapsed, setDelayElapsed] = useState(false);

useEffect(() => {
const timeoutId = setTimeout(() => {
setDelayElapsed(true);
}, 2000);

return () => {
clearTimeout(timeoutId);
};
}, []);

return delayElapsed ? <RedirectToSignIn /> : null;
};
<Unauthenticated>
<UnauthenticatedApp />
</Unauthenticated>

...

const UnauthenticatedApp = () => {
const [delayElapsed, setDelayElapsed] = useState(false);

useEffect(() => {
const timeoutId = setTimeout(() => {
setDelayElapsed(true);
}, 2000);

return () => {
clearTimeout(timeoutId);
};
}, []);

return delayElapsed ? <RedirectToSignIn /> : null;
};
But of course that's not ideal!
7 Replies
Michal Srb
Michal Srb2y ago
Hey RJ I’d have to play with this a bit to be sure how to approach it but one thing you could give a try is to do this without the convex helper components. If that’s working you can put a convex helper component inside the clerk one.
RJ
RJOP2y ago
Wrapping the Convex auth status in the Clerk auth status worked! But it's a bit tricky to get right (especially while making sure the loading state isn't re-rendered needlessly). Here's the code I used:
type ConvexAuthState =
| "ConvexAuthLoading"
| "ConvexAuthenticated"
| "ConvexUnauthenticated";

const convexAuthStateFromConvexAuth = (
convexAuth: ReturnType<typeof useConvexAuth>
): ConvexAuthState =>
match(convexAuth)
.with({ isLoading: true }, () => "ConvexAuthLoading" as const)
.with({ isAuthenticated: false }, () => "ConvexUnauthenticated" as const)
.with({ isAuthenticated: true }, () => "ConvexAuthenticated" as const)
.exhaustive();

type ClerkAuthState =
| "ClerkAuthLoading"
| "ClerkAuthenticated"
| "ClerkUnauthenticated";

const clerkAuthStateFromClerkAuth = (
clerkAuth: ReturnType<typeof useAuth>
): ClerkAuthState =>
match(clerkAuth)
.with({ isLoaded: false }, () => "ClerkAuthLoading" as const)
.with({ isSignedIn: false }, () => "ClerkUnauthenticated" as const)
.with({ isSignedIn: true }, () => "ClerkAuthenticated" as const)
.exhaustive();

const App = () => {
const clerkAuth = clerkAuthStateFromClerkAuth(useAuth());
const convexAuth = convexAuthStateFromConvexAuth(useConvexAuth());

return match([clerkAuth, convexAuth])
.with(
["ClerkAuthLoading", P.any],
[P.any, "ConvexAuthLoading"],
["ClerkAuthenticated", "ConvexUnauthenticated"],
() => <LoadingApp />
)
.with(["ClerkUnauthenticated", P.any], () => <RedirectToSignIn />)
.with(["ClerkAuthenticated", "ConvexAuthenticated"], () => (
<AuthenticatedApp />
))
.exhaustive();
};
type ConvexAuthState =
| "ConvexAuthLoading"
| "ConvexAuthenticated"
| "ConvexUnauthenticated";

const convexAuthStateFromConvexAuth = (
convexAuth: ReturnType<typeof useConvexAuth>
): ConvexAuthState =>
match(convexAuth)
.with({ isLoading: true }, () => "ConvexAuthLoading" as const)
.with({ isAuthenticated: false }, () => "ConvexUnauthenticated" as const)
.with({ isAuthenticated: true }, () => "ConvexAuthenticated" as const)
.exhaustive();

type ClerkAuthState =
| "ClerkAuthLoading"
| "ClerkAuthenticated"
| "ClerkUnauthenticated";

const clerkAuthStateFromClerkAuth = (
clerkAuth: ReturnType<typeof useAuth>
): ClerkAuthState =>
match(clerkAuth)
.with({ isLoaded: false }, () => "ClerkAuthLoading" as const)
.with({ isSignedIn: false }, () => "ClerkUnauthenticated" as const)
.with({ isSignedIn: true }, () => "ClerkAuthenticated" as const)
.exhaustive();

const App = () => {
const clerkAuth = clerkAuthStateFromClerkAuth(useAuth());
const convexAuth = convexAuthStateFromConvexAuth(useConvexAuth());

return match([clerkAuth, convexAuth])
.with(
["ClerkAuthLoading", P.any],
[P.any, "ConvexAuthLoading"],
["ClerkAuthenticated", "ConvexUnauthenticated"],
() => <LoadingApp />
)
.with(["ClerkUnauthenticated", P.any], () => <RedirectToSignIn />)
.with(["ClerkAuthenticated", "ConvexAuthenticated"], () => (
<AuthenticatedApp />
))
.exhaustive();
};
(match is pattern-matching via ts-pattern [https://github.com/gvergnaud/ts-pattern#about]) Part of what makes the above code so tricky is that both Convex and Clerk have overly-complex return signatures. To use Convex as an example, there are really only three possible states (as reflected in the exposed helper components) that Convex auth can be in:
"AuthLoading" | "Unauthenticated" | "Authenticated"
"AuthLoading" | "Unauthenticated" | "Authenticated"
But instead of returning a sum type, useConvexAuth returns instead a product type, which has four possible values:
{ isLoading: boolean;
isAuthenticated: boolean;
}
{ isLoading: boolean;
isAuthenticated: boolean;
}
The four possible types are { true, true }, { true, false }, { false, true } and { false, false }—or the number of possible values for each field (2 each, because they're each booleans) times the number of fields (2), 2 * 2 = 4 (hence the term "product type"). The one value which is nonsensical is:
{ isLoading: true,
isAuthenticated: true
}
{ isLoading: true,
isAuthenticated: true
}
So as an aside, it would be nice to fix in useConvexAuth! If you want some free labor community contribution, I would be happy to look into updating that now that y'all have open-sourced the Convex JS library.
Michal Srb
Michal Srb2y ago
Hey @RJ rereading all of this I think using
<Unauthenticated>
<RedirectToSignIn />
</Unauthenticated>
<Unauthenticated>
<RedirectToSignIn />
</Unauthenticated>
should actually work. Are you on latest version of Convex? There is one more fix in 0.14.1-alpha.0 but that should not be impacting you here (although you could try it out to see).
RJ
RJOP2y ago
I'm not, but I'm upgrading now, so I'll try it again once I'm finished with the upgrade. I've upgraded to 0.14.1 and this bug is still present @Michal Srb Actually, I only verified that it's present using useConvexAuth, let me verify that it's also present with the <Unauthenticated /> component Yeah, same behavior with the components
Mikael Lirbank
I have the same, or a similar problem. In the app I am working on we create a ghost user in the browser if the user is not authenticated (that will later be merged with the actual user when the user sign in or sign up). To do this, I need to know if the user is loading (undefined), anonymous (null), or authenticated (User). My getUser query uses const identity = await auth.getUserIdentity(); which only returns the user or null. So I can't know if it is loading or if there is no user. Eg on the client, the loading states are: Anonymous 1. undefined (loading) 2. null Authenticated 1. undefined (loading) 2. null <------ This should not happen 3. User Okay, I guess I can solve this with Clerk useUser() or useConvexAuth(). But oh man, so many options: const { user: clerkViewer } = useUser(); const convexViewer = useQuery("getViewer"); const convexAuth = useConvexAuth(); 🙃
Michal Srb
Michal Srb2y ago
Hey @Mikael Lirbank , useConvexAuth should be the authoritative source of truth for whether the client has loaded up the auth provider library and the information was passed to your Convex backend. If useConvexAuth returns isAuthenticated: true and your function's getUserIdentity returns null you should be guaranteed that the user is not currently logged in. @RJ any chance you could boil down the bug to a minimal repro codebase and push it to a github repo? I was not able to reproduce the issue.
RJ
RJOP2y ago
Yes, happy to give it a shot! Sorry to have you attempt repro without success here 😕

Did you find this page helpful?