-
Notifications
You must be signed in to change notification settings - Fork 408
poc: useAuth with uSES #7170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
poc: useAuth with uSES #7170
Changes from all commits
5be7bc9
bd6e858
4fabc46
71a0ad7
ae14da4
0526138
6bf564c
9e8a634
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,12 +8,12 @@ import type { | |
| SignOut, | ||
| UseAuthReturn, | ||
| } from '@clerk/shared/types'; | ||
| import { useCallback } from 'react'; | ||
| import { useCallback, useSyncExternalStore } from 'react'; | ||
|
|
||
| import { useAuthContext } from '../contexts/AuthContext'; | ||
| import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; | ||
| import { errorThrower } from '../errors/errorThrower'; | ||
| import { invalidStateError } from '../errors/messages'; | ||
| import { authStore } from '../stores/authStore'; | ||
| import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider'; | ||
| import { createGetToken, createSignOut } from './utils'; | ||
|
|
||
|
|
@@ -95,17 +95,12 @@ type UseAuthOptions = Record<string, any> | PendingSessionOptions | undefined | | |
| export const useAuth = (initialAuthStateOrOptions: UseAuthOptions = {}): UseAuthReturn => { | ||
| useAssertWrappedByClerkProvider('useAuth'); | ||
|
|
||
| const { treatPendingAsSignedOut, ...rest } = initialAuthStateOrOptions ?? {}; | ||
| const initialAuthState = rest as any; | ||
| const { treatPendingAsSignedOut } = initialAuthStateOrOptions ?? {}; | ||
|
|
||
| const authContextFromHook = useAuthContext(); | ||
| let authContext = authContextFromHook; | ||
| const isomorphicClerk = useIsomorphicClerkContext(); | ||
|
|
||
| if (authContext.sessionId === undefined && authContext.userId === undefined) { | ||
| authContext = initialAuthState != null ? initialAuthState : {}; | ||
| } | ||
|
Comment on lines
-104
to
-106
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to see if we can trust the authContext. We can just the the authStore directly here |
||
| const authContext = useSyncExternalStore(authStore.subscribe, authStore.getSnapshot, authStore.getServerSnapshot); | ||
|
|
||
| const isomorphicClerk = useIsomorphicClerkContext(); | ||
| const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]); | ||
| const signOut: SignOut = useCallback(createSignOut(isomorphicClerk), [isomorphicClerk]); | ||
|
|
||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,113 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { AuthContextValue } from '../contexts/AuthContext'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { IsomorphicClerk } from '../isomorphicClerk'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type AuthSnapshot = AuthContextValue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Listener = () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class AuthStore { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private listeners = new Set<() => void>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private currentSnapshot: AuthSnapshot; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private serverSnapshot: AuthSnapshot | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private clerkUnsubscribe: (() => void) | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.currentSnapshot = this.createEmptySnapshot(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connect(clerk: IsomorphicClerk) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.disconnect(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.clerkUnsubscribe = clerk.addListener(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.updateFromClerk(clerk); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.updateFromClerk(clerk); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disconnect() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.clerkUnsubscribe) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.clerkUnsubscribe(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.clerkUnsubscribe = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Set the SSR snapshot - must be called before hydration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setServerSnapshot(snapshot: AuthSnapshot) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.serverSnapshot = snapshot; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * For useSyncExternalStore - returns current client state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getSnapshot = (): AuthSnapshot => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.currentSnapshot; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * For useSyncExternalStore - returns SSR/hydration state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * React automatically uses this during SSR and hydration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getServerSnapshot = (): AuthSnapshot => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If we have a server snapshot, ALWAYS return it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // React will switch to getSnapshot after hydration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.serverSnapshot || this.currentSnapshot; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Add explicit return types to public methods. Per coding guidelines, public API methods should have explicit return types. While TypeScript can infer these, explicit annotations improve maintainability and API documentation. Apply this diff: - setServerSnapshot(snapshot: AuthSnapshot) {
+ setServerSnapshot(snapshot: AuthSnapshot): void {
this.serverSnapshot = snapshot;
}
- getSnapshot = (): AuthSnapshot => {
+ getSnapshot = (): Readonly<AuthSnapshot> => {
return this.currentSnapshot;
};
- getServerSnapshot = (): AuthSnapshot => {
+ getServerSnapshot = (): Readonly<AuthSnapshot> => {
return this.serverSnapshot || this.currentSnapshot;
};Note: Consider returning 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subscribe = (listener: Listener): (() => void) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.listeners.add(listener); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => this.listeners.delete(listener); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private updateFromClerk(clerk: IsomorphicClerk) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newSnapshot = this.transformClerkState(clerk); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Only notify if actually changed (reference equality is fine here) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (newSnapshot !== this.currentSnapshot) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.currentSnapshot = newSnapshot; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.notifyListeners(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+63
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reference equality check always fails—listeners notified on every update. Line 67 checks Implement shallow equality checking to only notify when actual values change: + private hasChanged(prev: AuthSnapshot, next: AuthSnapshot): boolean {
+ return (
+ prev.actor !== next.actor ||
+ prev.factorVerificationAge !== next.factorVerificationAge ||
+ prev.orgId !== next.orgId ||
+ prev.orgPermissions !== next.orgPermissions ||
+ prev.orgRole !== next.orgRole ||
+ prev.orgSlug !== next.orgSlug ||
+ prev.sessionClaims !== next.sessionClaims ||
+ prev.sessionId !== next.sessionId ||
+ prev.sessionStatus !== next.sessionStatus ||
+ prev.userId !== next.userId
+ );
+ }
+
private updateFromClerk(clerk: IsomorphicClerk): void {
const newSnapshot = this.transformClerkState(clerk);
- if (newSnapshot !== this.currentSnapshot) {
+ if (this.hasChanged(this.currentSnapshot, newSnapshot)) {
this.currentSnapshot = newSnapshot;
this.notifyListeners();
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private transformClerkState(clerk: IsomorphicClerk): AuthSnapshot { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const orgId = clerk.organization?.id; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const membership = clerk.organization | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? clerk.user?.organizationMemberships?.find(om => om.organization.id === orgId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actor: clerk.session?.actor, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| factorVerificationAge: clerk.session?.factorVerificationAge ?? null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orgId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orgPermissions: membership?.permissions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orgRole: membership?.role, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orgSlug: clerk.organization?.slug, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessionClaims: clerk.session?.lastActiveToken?.jwt?.claims, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessionId: clerk.session?.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessionStatus: clerk.session?.status, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: clerk.user?.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private createEmptySnapshot(): AuthSnapshot { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actor: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| factorVerificationAge: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orgId: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orgPermissions: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orgRole: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orgSlug: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessionClaims: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessionId: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessionStatus: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private notifyListeners() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.listeners.forEach(listener => listener()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const authStore = new AuthStore(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wont work since like
useEffect,useLayoutEffectdoesn't run on the server.The only way to make SSR work with something like this currently is to call this function in render. This is technically fine as long as the
serverSnapshotis not already set, since that counts as seeding a cache, but I'm not sure it's the best approach.Consider also the case of:
in Next. With a singleton
authStore, we can only have a single server snapshot, when what we want is one per subtree. I think this means we want to leverage React context. Somewhat like this PR does forinitialState.I envision it like:
const serverSnapshot = deriveServerSnapshot(initialState).