Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

React adapter — @aero2/sdk-react

Idiomatic React hooks and provider wrapping @aero2/sdk/browser. Mount <Aero2Provider> once; everything else is hooks.

Install

npm install @aero2/sdk @aero2/sdk-react

React 18+ is a peer dependency (works with 18 and 19).

Quickstart

import { Aero2Provider, useAuth } from "@aero2/sdk-react";
 
export function App() {
  return (
    <Aero2Provider
      config={{
        issuer: "https://your-app.aero2.dev",
        clientId: "your-spa-client-id",
        redirectUri: `${window.location.origin}/callback`,
      }}
      onCallbackSuccess={(appState) => {
        // Round-trip pre-login navigation. Pass `appState` to
        // `loginWithRedirect({ appState })` and read it here.
        const target = (appState as { returnTo?: string })?.returnTo ?? "/";
        window.history.replaceState({}, "", target);
      }}
    >
      <Nav />
      {/* ...rest of app */}
    </Aero2Provider>
  );
}
 
function Nav() {
  const { isLoading, isAuthenticated, login, logout } = useAuth();
  if (isLoading) return <Spinner />;
  return isAuthenticated ? (
    <button onClick={() => logout({ returnTo: "/" })}>Sign out</button>
  ) : (
    <button onClick={() => login()}>Sign in</button>
  );
}

What's shipped

ExportPurpose
<Aero2Provider>Context provider. Accepts client or config. Auto-handles ?code=… / ?error=… redirect URLs (toggle with autoHandleCallback={false}).
useAuth(){ isLoading, isAuthenticated, error, login, logout }.
useAccessToken(){ token, isLoading, error, refresh }. Re-runs on auth-state transitions.
useUser<T>(){ user, isLoading, error }. Generic on the userinfo shape.
<RequireAuth>Route guard. Redirects to loginWithRedirect() or renders an unauthenticated slot.

<Aero2Provider> props

PropTypeDescription
clientBrowserClientUse a pre-built client. Mutually exclusive with config.
configBrowserClientConfigProvider calls createBrowserClient(config) once.
autoHandleCallbackboolean (default true)When true, the provider runs handleRedirectCallback() automatically when it mounts on a URL with ?code=… or ?error=….
onCallbackSuccess(appState: unknown) => voidFires after the auto-handler succeeds. Common use: navigate to appState.returnTo.
onCallbackError(err: unknown) => voidFires when the auto-handler throws (e.g. user denied consent on the IdP).

useAuth()

const { isLoading, isAuthenticated, error, login, logout } = useAuth();
  • isLoadingtrue until the initial client.isAuthenticated() check resolves.
  • isAuthenticatedtrue only if there's a usable session (matches client.isAuthenticated() — see the browser SDK guide for the strict semantics).
  • error — any error thrown by the initial check (network failure, etc).
  • login(options?) — calls client.loginWithRedirect(options). Pass { appState: { returnTo: "/dashboard" } } to round-trip the pre-login location.
  • logout(options?) — calls client.logout(options) and bumps the provider's authVersion so consumers re-render.

login and logout are stable references — safe to pass directly to <button onClick={...}>.

useAccessToken()

function ProtectedFetcher() {
  const { token, isLoading, error, refresh } = useAccessToken();
  useEffect(() => {
    if (!token) return;
    fetch("/api/me", { headers: { Authorization: `Bearer ${token}` } });
  }, [token]);
  if (isLoading) return <Spinner />;
  if (error) return <button onClick={() => refresh()}>Retry</button>;
  return <Profile />;
}

The hook re-runs on auth-state transitions (post-callback, post-logout). It does NOT auto-retry on transient errors — surface error to the user and offer a button that calls refresh().

useUser<T>()

interface User { sub: string; email: string; roles: string[] }
 
function ProfileMenu() {
  const { user, isLoading } = useUser<User>();
  if (isLoading) return <Spinner />;
  if (!user) return null;
  return <span>{user.email}</span>;
}

Generic on the userinfo shape — pass your own User type for typed access to custom claims.

<RequireAuth>

Route guard. Either renders children (authenticated) or handles the unauthenticated case via one of two modes:

<Route
  path="/dashboard"
  element={
    <RequireAuth
      fallback={<Spinner />}
      loginOptions={{ appState: { returnTo: "/dashboard" } }}
    >
      <Dashboard />
    </RequireAuth>
  }
/>

Use mode="render" if you'd rather show a sign-in CTA than redirect immediately:

<RequireAuth mode="render" unauthenticated={<SignInPrompt />}>
  <ProtectedContent />
</RequireAuth>

SSR / non-browser environments

The provider is SSR-safe: it doesn't touch window until effects run on the client. On the server, useAuth() returns { isLoading: true, isAuthenticated: false }. For a different SSR fallback, pass a client constructed with storage: memoryStorage() and a stub windowImpl.

Why a separate package?

@aero2/sdk/browser is framework-agnostic. The React adapter exists so consumers get:

  1. No imperative wiring — hooks return the current state, not a callback you have to subscribe and clean up.
  2. Re-renders on transitions — the provider tracks an internal authVersion and bumps it on callback completion and logout(), so components stay in sync without a separate state-management layer.
  3. Stable referenceslogin / logout are memoized, so passing them to <button onClick={...}> doesn't churn child renders.

Roadmap

Larger UI components (<SignIn />, <UserProfile />, themed forms) are intentionally deferred. Hooks first; let teams style their own forms. If you need an unstyled headless approach we expose enough primitives to build any UI on top.