Skip to content

Conversation

@jacekradko
Copy link
Member

@jacekradko jacekradko commented Nov 6, 2025

Description

POC of useSyncExternalStore in useAuth to get around hydration mismatches on initial SSR -> CSR transfer

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • New Features

    • Extended authentication context with organization custom permissions support
    • Added factor verification age tracking to enhance security state management
  • Refactor

    • Modernized authentication state management with improved server-side rendering support
    • Enhanced real-time authentication synchronization for better state consistency across the application

@changeset-bot
Copy link

changeset-bot bot commented Nov 6, 2025

⚠️ No Changeset found

Latest commit: 9e8a634

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Nov 6, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
clerk-js-sandbox Ready Ready Preview Comment Nov 8, 2025 2:11am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 6, 2025

Walkthrough

These changes introduce a new external authentication state store (authStore) using React's useSyncExternalStore pattern. The AuthContext interface is extended with factorVerificationAge and orgPermissions fields. The ClerkContextProvider and useAuth hook are refactored to derive authentication state from the new store, replacing local context consumption with store-based subscriptions for SSR hydration support.

Changes

Cohort / File(s) Change Summary
Authentication Context Interface
packages/react/src/contexts/AuthContext.ts
Added factorVerificationAge: [number, number] | null and orgPermissions: OrganizationCustomPermissionKey[] | null | undefined fields. Reordered userId, sessionId, sessionStatus, and sessionClaims from top to bottom of the interface.
New Authentication Store
packages/react/src/stores/authStore.ts
New file introducing AuthStore class that manages authentication state snapshots via subscriptions to Clerk updates. Exports singleton authStore instance with connect(), disconnect(), setServerSnapshot(), getSnapshot(), getServerSnapshot(), and subscribe() methods to support SSR hydration and external store patterns.
Context Provider Refactor
packages/react/src/contexts/ClerkContextProvider.tsx
Refactored useLoadedIsomorphicClerk to return only isomorphicClerk (removed clerkStatus). Introduced useLayoutEffect with authStore initialization for SSR/hydration. Added useSyncExternalStore consumption to derive authValue from external store. Reorganized memoization strategy to depend on clerk state and authStore subscriptions instead of clerkStatus.
Hook Implementation
packages/react/src/hooks/useAuth.ts
Replaced useAuthContext with useSyncExternalStore and authStore for state derivation. Updated parameter type: initialAuthStateOrOptions now typed as UseAuthOptions (removed | null). Store now provides all authentication state; context fallback logic removed.
Hook Tests
packages/react/src/hooks/__tests__/useAuth.test.tsx
Added mock for authStore with getClientSnapshot, getServerSnapshot, and subscribe functions. Converted existing ClerkProvider rendering test to skipped variant.
Documentation Formatting
packages/shared/src/authorization.ts
Minor formatting changes to JSDoc comment blocks (added blank-line asterisks). No logic or type signature changes.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant CCP as ClerkContextProvider
    participant useAuth as useAuth Hook
    participant Store as authStore
    participant Clerk as IsomorphicClerk

    Note over CCP: SSR/Hydration Setup
    CCP->>Store: setServerSnapshot(initialState)
    CCP->>Store: connect(clerk)
    Store->>Clerk: subscribe to updates

    Note over CCP: Render Phase
    CCP->>Store: useSyncExternalStore listener
    Store-->>CCP: getSnapshot() / getServerSnapshot()

    App->>useAuth: useAuth()
    useAuth->>Store: useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
    Store-->>useAuth: authValue snapshot
    useAuth-->>App: { userId, sessionId, session, user, ... }

    Clerk->>Store: update event
    Store->>Store: transformClerkState()
    Store->>Store: notifyListeners()
    Store-->>useAuth: new snapshot
    useAuth->>App: re-render with updated auth
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Key areas requiring attention:
    • Verify authStore connection/disconnection lifecycle and listener cleanup in ClerkContextProvider to prevent memory leaks
    • Confirm SSR hydration flow: setServerSnapshot() timing and how initial state properly seeds the store before first render
    • Validate useSyncExternalStore usage in both useAuth and ClerkContextProvider to ensure consistent snapshot derivation
    • Review changes to useLoadedIsomorphicClerk return signature and confirm all consuming code is updated
    • Ensure transformClerkState correctly handles edge cases for the new factorVerificationAge and orgPermissions fields

Poem

🐰 Our store hops with state so fine,
External snapshots in perfect line,
SSR hydration flows just right,
AuthContext fields now rearranged bright,
Rabbit-approved patterns take flight! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'poc: useAuth with uSES' is partially related to the changeset—it refers to a real aspect of the change (useAuth and useSyncExternalStore), but is abbreviated/unclear and does not clearly summarize the main architectural change of introducing external store-based state management.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch poc/auth-store

Comment @coderabbitai help to get the list of available commands and usage tips.

@jacekradko jacekradko changed the title poc: authStore poc: useAuth with uSES Nov 6, 2025
Comment on lines -104 to -106
if (authContext.sessionId === undefined && authContext.userId === undefined) {
authContext = initialAuthState != null ? initialAuthState : {};
}
Copy link
Member Author

Choose a reason for hiding this comment

The 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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 8, 2025

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7170

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7170

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7170

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7170

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7170

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7170

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@7170

@clerk/clerk-expo

npm i https://pkg.pr.new/@clerk/clerk-expo@7170

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7170

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7170

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7170

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7170

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7170

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7170

@clerk/clerk-react

npm i https://pkg.pr.new/@clerk/clerk-react@7170

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7170

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@7170

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7170

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7170

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7170

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@7170

@clerk/types

npm i https://pkg.pr.new/@clerk/types@7170

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7170

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7170

commit: 9e8a634

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/react/src/contexts/ClerkContextProvider.tsx (1)

104-128: Remove unused clerkStatus state to prevent unnecessary re-renders.

The clerkStatus state is maintained and returned but never used (line 28 only destructures isomorphicClerk). This causes unnecessary re-renders whenever clerk status changes.

Apply this diff:

 const useLoadedIsomorphicClerk = (options: IsomorphicClerkOptions) => {
   const isomorphicClerkRef = React.useRef(IsomorphicClerk.getOrCreateInstance(options));
-  const [clerkStatus, setClerkStatus] = React.useState(isomorphicClerkRef.current.status);
 
   React.useEffect(() => {
     void isomorphicClerkRef.current.__unstable__updateProps({ appearance: options.appearance });
   }, [options.appearance]);
 
   React.useEffect(() => {
     void isomorphicClerkRef.current.__unstable__updateProps({ options });
   }, [options]);
 
   React.useEffect(() => {
     const clerk = isomorphicClerkRef.current;
-    clerk.on('status', setClerkStatus);
     return () => {
-      if (clerk) {
-        clerk.off('status', setClerkStatus);
-      }
       IsomorphicClerk.clearInstance();
     };
   }, []);
 
-  return { isomorphicClerk: isomorphicClerkRef.current, clerkStatus };
+  return { isomorphicClerk: isomorphicClerkRef.current };
 };
packages/react/src/hooks/__tests__/useAuth.test.tsx (1)

99-109: Remove the skipped test or rewrite it to align with the new authStore pattern.

This test predates the refactoring to use useSyncExternalStore with an external store. The implementation (line 102 of useAuth.ts) now retrieves auth state directly from authStore via useSyncExternalStore, making manual context provider wrapping obsolete. Either remove this test entirely since the error-handling case is already validated by the first test, or rewrite it to verify the hook works correctly when authStore is properly initialized—though note that testing useSyncExternalStore integration typically requires full provider setup rather than component wrapping.

Also note: The test mock uses non-standard method names (getClientSnapshot, getServerSnapshot) that don't match the actual store interface (getSnapshot, getServerSnapshot).

🧹 Nitpick comments (1)
packages/react/src/stores/authStore.ts (1)

8-8: Use the Listener type alias consistently.

Line 5 defines type Listener = () => void; but line 8 declares listeners with the inline type. For consistency and maintainability, use the type alias.

Apply this diff:

-  private listeners = new Set<() => void>();
+  private listeners = new Set<Listener>();
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 539fad7 and 9e8a634.

📒 Files selected for processing (6)
  • packages/react/src/contexts/AuthContext.ts (1 hunks)
  • packages/react/src/contexts/ClerkContextProvider.tsx (4 hunks)
  • packages/react/src/hooks/__tests__/useAuth.test.tsx (2 hunks)
  • packages/react/src/hooks/useAuth.ts (2 hunks)
  • packages/react/src/stores/authStore.ts (1 hunks)
  • packages/shared/src/authorization.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

**/*.{js,jsx,ts,tsx}: All code must pass ESLint checks with the project's configuration
Follow established naming conventions (PascalCase for components, camelCase for variables)
Maintain comprehensive JSDoc comments for public APIs
Use dynamic imports for optional features
All public APIs must be documented with JSDoc
Provide meaningful error messages to developers
Include error recovery suggestions where applicable
Log errors appropriately for debugging
Lazy load components and features when possible
Implement proper caching strategies
Use efficient data structures and algorithms
Profile and optimize critical paths
Validate all inputs and sanitize outputs
Implement proper logging with different levels

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
  • packages/shared/src/authorization.ts
  • packages/react/src/stores/authStore.ts
  • packages/react/src/hooks/useAuth.ts
  • packages/react/src/contexts/ClerkContextProvider.tsx
  • packages/react/src/contexts/AuthContext.ts
**/*.{js,jsx,ts,tsx,json,css,scss,md,yaml,yml}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use Prettier for consistent code formatting

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
  • packages/shared/src/authorization.ts
  • packages/react/src/stores/authStore.ts
  • packages/react/src/hooks/useAuth.ts
  • packages/react/src/contexts/ClerkContextProvider.tsx
  • packages/react/src/contexts/AuthContext.ts
packages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

TypeScript is required for all packages

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
  • packages/shared/src/authorization.ts
  • packages/react/src/stores/authStore.ts
  • packages/react/src/hooks/useAuth.ts
  • packages/react/src/contexts/ClerkContextProvider.tsx
  • packages/react/src/contexts/AuthContext.ts
packages/**/*.{ts,tsx,d.ts}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Packages should export TypeScript types alongside runtime code

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
  • packages/shared/src/authorization.ts
  • packages/react/src/stores/authStore.ts
  • packages/react/src/hooks/useAuth.ts
  • packages/react/src/contexts/ClerkContextProvider.tsx
  • packages/react/src/contexts/AuthContext.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

Use proper TypeScript error types

**/*.{ts,tsx}: Always define explicit return types for functions, especially public APIs
Use proper type annotations for variables and parameters where inference isn't clear
Avoid any type - prefer unknown when type is uncertain, then narrow with type guards
Use interface for object shapes that might be extended
Use type for unions, primitives, and computed types
Prefer readonly properties for immutable data structures
Use private for internal implementation details
Use protected for inheritance hierarchies
Use public explicitly for clarity in public APIs
Prefer readonly for properties that shouldn't change after construction
Prefer composition and interfaces over deep inheritance chains
Use mixins for shared behavior across unrelated classes
Implement dependency injection for loose coupling
Let TypeScript infer when types are obvious
Use const assertions for literal types: as const
Use satisfies operator for type checking without widening
Use mapped types for transforming object types
Use conditional types for type-level logic
Leverage template literal types for string manipulation
Use ES6 imports/exports consistently
Use default exports sparingly, prefer named exports
Use type-only imports: import type { ... } from ...
No any types without justification
Proper error handling with typed errors
Consistent use of readonly for immutable data
Proper generic constraints
No unused type parameters
Proper use of utility types instead of manual type construction
Type-only imports where possible
Proper tree-shaking friendly exports
No circular dependencies
Efficient type computations (avoid deep recursion)

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
  • packages/shared/src/authorization.ts
  • packages/react/src/stores/authStore.ts
  • packages/react/src/hooks/useAuth.ts
  • packages/react/src/contexts/ClerkContextProvider.tsx
  • packages/react/src/contexts/AuthContext.ts
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development.mdc)

**/*.{jsx,tsx}: Use error boundaries in React components
Minimize re-renders in React components

**/*.{jsx,tsx}: Always use functional components with hooks instead of class components
Follow PascalCase naming for components: UserProfile, NavigationMenu
Keep components focused on a single responsibility - split large components
Limit component size to 150-200 lines; extract logic into custom hooks
Use composition over inheritance - prefer smaller, composable components
Export components as named exports for better tree-shaking
One component per file with matching filename and component name
Use useState for simple state management
Use useReducer for complex state logic
Implement proper state initialization
Use proper state updates with callbacks
Implement proper state cleanup
Use Context API for theme/authentication
Implement proper state selectors
Use proper state normalization
Implement proper state persistence
Use React.memo for expensive components
Implement proper useCallback for handlers
Use proper useMemo for expensive computations
Implement proper virtualization for lists
Use proper code splitting with React.lazy
Implement proper cleanup in useEffect
Use proper refs for DOM access
Implement proper event listener cleanup
Use proper abort controllers for fetch
Implement proper subscription cleanup
Use proper HTML elements
Implement proper ARIA attributes
Use proper heading hierarchy
Implement proper form labels
Use proper button types
Implement proper focus management
Use proper keyboard shortcuts
Implement proper tab order
Use proper skip links
Implement proper focus traps
Implement proper error boundaries
Use proper error logging
Implement proper error recovery
Use proper error messages
Implement proper error fallbacks
Use proper form validation
Implement proper error states
Use proper error messages
Implement proper form submission
Use proper form reset
Use proper component naming
Implement proper file naming
Use proper prop naming
Implement proper...

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
  • packages/react/src/contexts/ClerkContextProvider.tsx
packages/**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo.mdc)

Unit tests should use Jest or Vitest as the test runner.

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo.mdc)

Support multiple Clerk environment variables (CLERK_, NEXT_PUBLIC_CLERK_, etc.) for configuration.

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
  • packages/shared/src/authorization.ts
  • packages/react/src/stores/authStore.ts
  • packages/react/src/hooks/useAuth.ts
  • packages/react/src/contexts/ClerkContextProvider.tsx
  • packages/react/src/contexts/AuthContext.ts
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/react.mdc)

**/*.tsx: Use proper type definitions for props and state
Leverage TypeScript's type inference where possible
Use proper event types for handlers
Implement proper generic types for reusable components
Use proper type guards for conditional rendering

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
  • packages/react/src/contexts/ClerkContextProvider.tsx
**/*.test.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/react.mdc)

**/*.test.{jsx,tsx}: Use React Testing Library
Test component behavior, not implementation
Use proper test queries
Implement proper test isolation
Use proper test coverage
Test component interactions
Use proper test data
Implement proper test setup
Use proper test cleanup
Implement proper test assertions
Use proper test structure

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
**/__tests__/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/typescript.mdc)

**/__tests__/**/*.{ts,tsx}: Create type-safe test builders/factories
Use branded types for test isolation
Implement proper mock types that match interfaces

Files:

  • packages/react/src/hooks/__tests__/useAuth.test.tsx
🧬 Code graph analysis (3)
packages/react/src/stores/authStore.ts (1)
packages/react/src/contexts/AuthContext.ts (1)
  • AuthContextValue (10-21)
packages/react/src/hooks/useAuth.ts (1)
packages/react/src/stores/authStore.ts (1)
  • authStore (113-113)
packages/react/src/contexts/ClerkContextProvider.tsx (3)
packages/shared/src/deriveState.ts (1)
  • deriveState (15-20)
packages/react/src/stores/authStore.ts (1)
  • authStore (113-113)
packages/react/src/isomorphicClerk.ts (3)
  • session (695-701)
  • user (703-709)
  • organization (711-717)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (27)
  • GitHub Check: Integration Tests (quickstart, chrome, 16)
  • GitHub Check: Integration Tests (nextjs, chrome, 14)
  • GitHub Check: Integration Tests (nextjs, chrome, 15)
  • GitHub Check: Integration Tests (custom, chrome)
  • GitHub Check: Integration Tests (quickstart, chrome, 15)
  • GitHub Check: Integration Tests (nextjs, chrome, 16)
  • GitHub Check: Integration Tests (react-router, chrome)
  • GitHub Check: Integration Tests (billing, chrome)
  • GitHub Check: Integration Tests (machine, chrome)
  • GitHub Check: Integration Tests (tanstack-react-start, chrome)
  • GitHub Check: Integration Tests (handshake, chrome)
  • GitHub Check: Integration Tests (nuxt, chrome)
  • GitHub Check: Integration Tests (sessions, chrome)
  • GitHub Check: Integration Tests (localhost, chrome)
  • GitHub Check: Integration Tests (vue, chrome)
  • GitHub Check: Integration Tests (expo-web, chrome)
  • GitHub Check: Integration Tests (astro, chrome)
  • GitHub Check: Integration Tests (handshake:staging, chrome)
  • GitHub Check: Integration Tests (sessions:staging, chrome)
  • GitHub Check: Integration Tests (elements, chrome)
  • GitHub Check: Integration Tests (ap-flows, chrome)
  • GitHub Check: Integration Tests (generic, chrome)
  • GitHub Check: Integration Tests (express, chrome)
  • GitHub Check: Unit Tests (22, **)
  • GitHub Check: Publish with pkg-pr-new
  • GitHub Check: Static analysis
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (9)
packages/shared/src/authorization.ts (1)

78-78: Non-functional JSDoc formatting improvements.

The blank-line asterisks in the JSDoc blocks (lines 78, 166, 242) improve readability by separating description paragraphs. These changes are consistent with the broader documentation improvements mentioned in the PR context and align with JSDoc style best practices.

Also applies to: 166-166, 242-242

packages/react/src/hooks/__tests__/useAuth.test.tsx (1)

27-55: LGTM! Mock structure aligns with useSyncExternalStore pattern.

The mock correctly provides getClientSnapshot, getServerSnapshot, and subscribe methods expected by useSyncExternalStore. Returning identical snapshots for client and server is appropriate for these tests.

packages/react/src/stores/authStore.ts (2)

17-32: LGTM! Lifecycle methods correctly handle subscription management.

The connect method safely handles multiple calls by disconnecting first, and immediately syncs state with updateFromClerk. The disconnect method properly cleans up the subscription.


73-111: LGTM! State transformation correctly handles optional properties.

The transformClerkState method safely accesses optional Clerk properties using optional chaining, and correctly derives organization membership data. The nullish coalescing for factorVerificationAge ensures a consistent null default.

packages/react/src/hooks/useAuth.ts (2)

95-119: LGTM! Correctly implements external store pattern for SSR hydration.

The hook now derives authentication state from authStore using useSyncExternalStore, which properly handles SSR and client-side hydration. This addresses the past review comment about trusting the auth context by using the store directly as the source of truth.

Based on past review comments.


23-23: Remove | null from UseAuthOptions type definition or document its purpose.

No usages of useAuth(null) exist in the codebase. The parameter is handled identically for both null and undefined via the nullish coalescing operator (initialAuthStateOrOptions ?? {}), making the | null union member redundant. Modern React hooks use only undefined for optional parameters. If this was added for backward compatibility, consider documenting it; otherwise, remove it to align with React conventions and simplify the type.

packages/react/src/contexts/AuthContext.ts (1)

10-21: LGTM! Interface expanded to support enhanced auth state.

The new factorVerificationAge and orgPermissions fields align with the authentication state managed by authStore. Field reordering doesn't affect functionality.

packages/react/src/contexts/ClerkContextProvider.tsx (2)

46-67: LGTM! SSR hydration correctly initializes authStore.

The useLayoutEffect properly:

  • Sets the server snapshot before hydration when initialState is present
  • Connects the store to Clerk for live updates
  • Cleans up the subscription on unmount

This ensures consistent auth state during SSR → CSR transition.


69-79: LGTM! Client-side auth state correctly synchronized with external store.

Using useSyncExternalStore ensures consistent auth state across SSR/hydration/client transitions, and all context values are properly memoized to prevent unnecessary re-renders.

Comment on lines +37 to +56
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;
};
Copy link
Contributor

Choose a reason for hiding this comment

The 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 Readonly<AuthSnapshot> to prevent consumers from mutating the snapshot directly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
};
setServerSnapshot(snapshot: AuthSnapshot): void {
this.serverSnapshot = snapshot;
}
/**
* For useSyncExternalStore - returns current client state
*/
getSnapshot = (): Readonly<AuthSnapshot> => {
return this.currentSnapshot;
};
/**
* For useSyncExternalStore - returns SSR/hydration state
* React automatically uses this during SSR and hydration
*/
getServerSnapshot = (): Readonly<AuthSnapshot> => {
// If we have a server snapshot, ALWAYS return it
// React will switch to getSnapshot after hydration
return this.serverSnapshot || this.currentSnapshot;
};
🤖 Prompt for AI Agents
In packages/react/src/stores/authStore.ts around lines 37 to 56, the public
methods getSnapshot, getServerSnapshot and setServerSnapshot lack explicit
return type annotations; update their signatures to include explicit return
types (use Readonly<AuthSnapshot> for getters and void for the setter) so the
public API is typed explicitly and prevents consumers from mutating the returned
snapshot.

Comment on lines +63 to +71
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();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Reference equality check always fails—listeners notified on every update.

Line 67 checks newSnapshot !== this.currentSnapshot, but transformClerkState always creates a new object (line 64), so reference equality will always fail. This causes listeners to be notified on every Clerk update, even when auth state hasn't changed, leading to unnecessary re-renders.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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();
}
}
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);
// Only notify if actually changed (reference equality is fine here)
if (this.hasChanged(this.currentSnapshot, newSnapshot)) {
this.currentSnapshot = newSnapshot;
this.notifyListeners();
}
}
🤖 Prompt for AI Agents
In packages/react/src/stores/authStore.ts around lines 63 to 71, the current
reference equality check always fails because transformClerkState creates a new
object each call, causing listeners to be notified on every update; replace the
reference check with a shallow equality comparison of the snapshot's top-level
properties (compare primitives and object references for the same keys) and only
assign this.currentSnapshot and call notifyListeners if the shallow comparison
detects any differences; either reuse an existing shallowEqual utility or add a
small helper here that iterates keys and compares values, then use its result
instead of !==.

@blacksmith-sh
Copy link

blacksmith-sh bot commented Nov 8, 2025

Found 61 test failures on Blacksmith runners:

Test View Logs
[chrome] › integration/tests/
billing-hooks.test.ts:40:9 › billing hooks @billing › long-running--withBilling.next.ap
pRouter › when signed in › subscribes to a plan
View Logs
[chrome] › integration/tests/
components.test.ts:102:9 › component smoke tests @generic › long-running--next.appRoute
r.withEmailCodes › OrganizationSwitcher supports fallback
View Logs
[chrome] › integration/tests/
components.test.ts:102:9 › component smoke tests @generic › long-running--next.appRoute
r.withEmailCodes › SignIn supports fallback
View Logs
[chrome] › integration/tests/
components.test.ts:102:9 › component smoke tests @generic › long-running--react.vite.wi
thEmailCodes › CreateOrganization supports fallback
View Logs
[chrome] › integration/tests/
components.test.ts:102:9 › component smoke tests @generic › long-running--react.vite.wi
thEmailCodes › OrganizationProfile supports fallback
View Logs
[chrome] › integration/tests/
components.test.ts:102:9 › component smoke tests @generic › long-running--react.vite.wi
thEmailCodes › OrganizationSwitcher supports fallback
View Logs
[chrome] › integration/tests/
components.test.ts:102:9 › component smoke tests @generic › long-running--react.vite.wi
thEmailCodes › UserAvatar supports fallback
View Logs
[chrome] › integration/tests/
components.test.ts:102:9 › component smoke tests @generic › long-running--react.vite.wi
thEmailCodes › UserButton supports fallback
View Logs
[chrome] › integration/tests/
components.test.ts:102:9 › component smoke tests @generic › long-running--react.vite.wi
thEmailCodes › UserProfile supports fallback
View Logs
[chrome] › integration/tests/
content-security-policy.test.ts:10:9 › Content Security Policy @nextjs › long-running--
next.appRouter.withEmailCodes › Clerk loads when nonce is specified
View Logs
[chrome] › integration/tests/
content-security-policy.test.ts:10:9 › Content Security Policy @nextjs › long-running--
next.appRouter.withEmailCodes › Clerk loads when nonce is specified
View Logs
[chrome] › integration/tests/
content-security-policy.test.ts:10:9 › Content Security Policy @nextjs › long-running--
next.appRouter.withEmailCodes › Clerk loads when nonce is specified
View Logs
[chrome] › integration/tests/custom-flows/
sign-up.test.ts:53:7 › Custom Flows Sign Up @custom › can sign up with email and passwo
rd
View Logs
[chrome] › integration/tests/
custom-pages.test.ts:136:13 › user profile custom pages @generic › long-running--react.
vite.withEmailCodes › Custom pages coming from <UserButton/>
› user profile custom external absolute link
View Logs
[chrome] › integration/tests/
custom-pages.test.ts:154:13 › user profile custom pages @generic › long-running--react.
vite.withEmailCodes › Custom pages coming from <UserProfile/>
› user profile custom internal relative link
View Logs
[chrome] › integration/tests/
custom-pages.test.ts:65:13 › user profile custom pages @generic › long-running--react.v
ite.withEmailCodes › Custom pages coming from <UserButton/>
› user profile has all custom pages with icons in the side nav with specified order
View Logs
[chrome] › integration/tests/
custom-pages.test.ts:91:13 › user profile custom pages @generic › long-running--react.v
ite.withEmailCodes › Custom pages coming from <UserProfile/>
› user profile custom page 1
View Logs
[chrome] › integration/tests/
dynamic-keys.test.ts:92:7 › dynamic keys @nextjs › calls clerkClient with dynamic key
s from middleware runtime
View Logs
[chrome] › integration/tests/
dynamic-keys.test.ts:92:7 › dynamic keys @nextjs › calls clerkClient with dynamic key
s from middleware runtime
View Logs
[chrome] › integration/tests/expo-web/
basic.test.ts:35:9 › basic tests for expo web @expo-web › long-running--expo.expo-web ›
can sign in and user button renders
View Logs
[chrome] › integration/tests/expo-web/
custom-flows.test.ts:35:9 › custom flows test suite @expo-web › long-running--expo.expo
-web › sign in using custom flow
View Logs
[chrome] › integration/tests/expo-web/
custom-flows.test.ts:55:9 › custom flows test suite @expo-web › long-running--expo.expo
-web › sign up using custom flow and also delete user
View Logs
[chrome] › integration/tests/machine-auth/
component.test.ts:29:7 › api keys component @machine › long-running--withMachine.next.a
ppRouter › can create api keys
View Logs
[chrome] › integration/tests/next-account-portal/
clerk-v5-ap-core-1.test.ts:30:7 › Next with ClerkJS V5 <->
Account Portal Core 1 @ap-flows › sign in
View Logs
[chrome] › integration/tests/next-account-portal/
clerk-v5-ap-core-2.test.ts:30:7 › Next with ClerkJS V5 <->
Account Portal Core 2 @ap-flows › sign in
View Logs
[chrome] › integration/tests/
next-quickstart.test.ts:23:7 › nextjs @quickstart › long-running--quickstart.next.appRo
uter › Clerk client loads on first visit and Sign In button renders
View Logs
[chrome] › integration/tests/
next-quickstart.test.ts:23:7 › nextjs @quickstart › long-running--quickstart.next.appRo
uter › Clerk client loads on first visit and Sign In button renders
View Logs
[chrome] › integration/tests/
next-quickstart.test.ts:34:7 › nextjs @quickstart › long-running--quickstart.next.appRo
uter › can sign in with email and password
View Logs
[chrome] › integration/tests/
next-quickstart.test.ts:34:7 › nextjs @quickstart › long-running--quickstart.next.appRo
uter › can sign in with email and password
View Logs
[chrome] › integration/tests/
pricing-table.test.ts:250:7 › pricing table @billing › long-running--withBilling.next.a
ppRouter › starts free trial subscription for new user
View Logs
[chrome] › integration/tests/
pricing-table.test.ts:26:9 › pricing table @billing › long-running--withBilling.next.ap
pRouter › when signed out › renders pricing table with plans
View Logs
[chrome] › integration/tests/
pricing-table.test.ts:385:7 › pricing table @billing › long-running--withBilling.next.a
ppRouter › subscribing to other paid plans while on free trial is immediate cancellatio
n
View Logs
[chrome] › integration/tests/
pricing-table.test.ts:54:9 › pricing table @billing › long-running--withBilling.next.ap
pRouter › when signed out › when signed out, clicking subscribe button navigates to sig
n in page
View Logs
[chrome] › integration/tests/
pricing-table.test.ts:80:9 › pricing table @billing › long-running--withBilling.next.ap
pRouter › when signed in flow › when signed in, clicking get started button opens check
out drawer and shows free plan as active
View Logs
[chrome] › integration/tests/
protect.test.ts:38:7 › authorization @nextjs › long-running--next.appRouter.withCustomR
oles › Protect in RSCs and RCCs as admin
View Logs
[chrome] › integration/tests/
protect.test.ts:38:7 › authorization @nextjs › long-running--next.appRouter.withCustomR
oles › Protect in RSCs and RCCs as admin
View Logs
[chrome] › integration/tests/
protect.test.ts:38:7 › authorization @nextjs › long-running--next.appRouter.withCustomR
oles › Protect in RSCs and RCCs as admin
View Logs
[chrome] › integration/tests/react-router/
basic.test.ts:35:9 › basic tests for @react-router with middleware › long-running--reac
t-router.node › can sign in and user button renders
View Logs
[chrome] › integration/tests/react-router/
basic.test.ts:53:9 › basic tests for @react-router with middleware › long-running--reac
t-router.node › redirects to sign-in when unauthenticated
View Logs
[chrome] › integration/tests/react-router/
basic.test.ts:61:9 › basic tests for @react-router with middleware › long-running--reac
t-router.node › renders control components contents
View Logs
[chrome] › integration/tests/react-router/
basic.test.ts:74:9 › basic tests for @react-router with middleware › long-running--reac
t-router.node › renders user profile with SSR data
View Logs
[chrome] › integration/tests/react-router/
library-mode.test.ts:40:7 › Library Mode basic tests for @react-router › should log in
successfully
View Logs
[chrome] › integration/tests/react-router/
pre-middleware.test.ts:113:7 › basic tests for @react-router without middleware › can s
ign in and user button renders
View Logs
[chrome] › integration/tests/react-router/
pre-middleware.test.ts:131:7 › basic tests for @react-router without middleware › redir
ects to sign-in when unauthenticated
View Logs
[chrome] › integration/tests/react-router/
pre-middleware.test.ts:139:7 › basic tests for @react-router without middleware › rende
rs control components contents
View Logs
[chrome] › integration/tests/react-router/
pre-middleware.test.ts:152:7 › basic tests for @react-router without middleware › rende
rs user profile with SSR data
View Logs
[chrome] › integration/tests/
reverification.test.ts:217:11 › @nextjs require @reverification › long-running--next.ap
pRouter.withReverification › reverification error from Action
View Logs
[chrome] › integration/tests/
reverification.test.ts:217:11 › @nextjs require @reverification › long-running--next.ap
pRouter.withReverification › reverification error from Action
View Logs
[chrome] › integration/tests/
reverification.test.ts:217:11 › @nextjs require @reverification › long-running--next.ap
pRouter.withReverification › reverification error from Action
View Logs
[chrome] › integration/tests/
reverification.test.ts:250:11 › @nextjs require @reverification › long-running--next.ap
pRouter.withReverification › reverification recovery from Action
View Logs
[chrome] › integration/tests/
reverification.test.ts:250:11 › @nextjs require @reverification › long-running--next.ap
pRouter.withReverification › reverification recovery from Action
View Logs
[chrome] › integration/tests/
reverification.test.ts:250:11 › @nextjs require @reverification › long-running--next.ap
pRouter.withReverification › reverification recovery from Action
View Logs
[chrome] › integration/tests/
session-tasks-multi-session.test.ts:33:9 › session tasks multi-session flow @nextjs › l
ong-running--next.appRouter.withSessionTasks › when switching sessions, navigate to tas
k
View Logs
[chrome] › integration/tests/
session-tasks-multi-session.test.ts:33:9 › session tasks multi-session flow @nextjs › l
ong-running--next.appRouter.withSessionTasks › when switching sessions, navigate to tas
k
View Logs
[chrome] › integration/tests/
session-tasks-sign-up.test.ts:79:9 › session tasks after sign-up flow @nextjs › long-ru
nning--next.appRouter.withSessionTasks › with sso, navigate to task on after sign-up
View Logs
[chrome] › integration/tests/
sign-up-flow.test.ts:96:7 › sign up flow @generic @nextjs › long-running--next.appRoute
r.withEmailCodes › (modal) can sign up with phone number
View Logs
[chrome] › integration/tests/tanstack-start/
basic.test.ts:43:9 › basic tests for TanStack Start @tanstack-react-start › long-runnin
g--tanstack.react-start › can sign in and user button renders
View Logs
[chrome] › integration/tests/tanstack-start/
basic.test.ts:61:9 › basic tests for TanStack Start @tanstack-react-start › long-runnin
g--tanstack.react-start › clerk handler has ran
View Logs
[chrome] › integration/tests/tanstack-start/
basic.test.ts:77:9 › basic tests for TanStack Start @tanstack-react-start › long-runnin
g--tanstack.react-start › retrieve auth state in server functions
View Logs
[chrome] › integration/tests/tanstack-start/
basic.test.ts:96:9 › basic tests for TanStack Start @tanstack-react-start › long-runnin
g--tanstack.react-start › clerk handler sets headers
View Logs
[setup] › integration/tests/
global.setup.ts:7:6 › start long running apps ─────────────────────
View Logs


Fix in Cursor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants