Files
skills/docs/ui/react/react-auth/src/context/auth-context.md
2026-05-29 08:20:56 +01:00

4.2 KiB

auth-context

(no submodules)

Purpose

auth-context.tsx exposes the React Context surface for the auth flow. It defines AuthContext, the AuthProvider component, and the useAuth() hook. The provider owns an AuthClient instance (either injected or constructed from config) and a state block of { user, session, isLoading, isAuthenticated, error }. On mount (unless skipInitialCheck is set) it fetches the current session, normalises it through transformSessionResponse, and settles the state. It exposes eleven action callbacks (signIn, signUp, signOut, updateProfile, changePassword, forgotPassword, resetPassword, verifyEmail, resendVerificationEmail, deleteAccount, refreshSession) — every action wraps client calls with loading/error state mutations, re-fetches the session where appropriate, records the corresponding OTel counter, and re-throws on failure so callers can catch for form-level UI handling. useAuth() outside an AuthProvider throws ReactAuthContextMissingError so the failure is immediate and typed rather than a downstream undefined access.

Features

  • AuthContext + AuthProvider + useAuth() triple.
  • 11 typed action callbacks; each settles isLoading and surfaces error deterministically.
  • skipInitialCheck flag short-circuits the mount-time session fetch (test surface).
  • onAuthStateChange callback fires on every state transition (useful for analytics).
  • Re-throws action errors so form-level UI can render messages; never silently swallows.

Usage: Basic

import { AuthProvider, useAuth } from "@suaveplan/react-auth";

function Inside() {
    const { user, signOut } = useAuth();
    return user ? <button onClick={() => signOut()}>Sign out</button> : null;
}

<AuthProvider config={{ baseURL: "https://auth.example.com" }}>
    <Inside />
</AuthProvider>;

Usage: Advanced

Inject a pre-built client and observe state changes:

import {
    AuthProvider,
    createAuthClient,
    type AuthState,
} from "@suaveplan/react-auth";

const client = createAuthClient({
    baseURL: "https://auth.example.com",
    siteDomain: "tenant-a",
});

function onAuthStateChange(state: AuthState) {
    analytics.track("auth_state", { authenticated: state.isAuthenticated });
}

<AuthProvider
    config={{ baseURL: "https://auth.example.com" }}
    client={client}
    onAuthStateChange={onAuthStateChange}
>
    {/* ... */}
</AuthProvider>;

API Reference

  • interface AuthState — { user: User | null; session: Session | null; isLoading: boolean; isAuthenticated: boolean; error: Error | null }.
  • interface AuthContextValue extends AuthState — adds client: AuthClient plus the 11 action callbacks.
  • interface AuthProviderProps — { children: ReactNode; config: AuthClientConfig; client?: AuthClient; onAuthStateChange?: (state: AuthState) => void; skipInitialCheck?: boolean }.
  • const AuthContext: React.Context<AuthContextValue | undefined> — context handle.
  • function AuthProvider(props: AuthProviderProps): ReactNode — wraps children with the context.
  • function useAuth(): AuthContextValue — reads the context; throws ReactAuthContextMissingError when outside an AuthProvider.

Implementation Notes

State is consolidated in a single useState block so transitions are atomic — callers never see a half-updated value. Every action wraps its critical section in try/catch/finally-style logic but with explicit setState calls rather than a finally clause, because the success and failure branches set different sets of fields. Caught values pass through toError(value) which wraps non-Error throwables so the error field is always a real Error (this avoids violating biome's useThrowOnlyError rule when a downstream surface re-throws from the context). Session fetches go through timedSessionFetch which records the wall-clock duration into the react_auth.session_fetch.duration_ms histogram with a success/failure label. The mounted ref guards the initial-load effect from setting state after unmount; React 19's StrictMode double-mount runs the cleanup before the second mount, so the first effect's mounted = false is honoured.