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-reactReact 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
| Export | Purpose |
|---|---|
<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
| Prop | Type | Description |
|---|---|---|
client | BrowserClient | Use a pre-built client. Mutually exclusive with config. |
config | BrowserClientConfig | Provider calls createBrowserClient(config) once. |
autoHandleCallback | boolean (default true) | When true, the provider runs handleRedirectCallback() automatically when it mounts on a URL with ?code=… or ?error=…. |
onCallbackSuccess | (appState: unknown) => void | Fires after the auto-handler succeeds. Common use: navigate to appState.returnTo. |
onCallbackError | (err: unknown) => void | Fires when the auto-handler throws (e.g. user denied consent on the IdP). |
useAuth()
const { isLoading, isAuthenticated, error, login, logout } = useAuth();isLoading—trueuntil the initialclient.isAuthenticated()check resolves.isAuthenticated—trueonly if there's a usable session (matchesclient.isAuthenticated()— see the browser SDK guide for the strict semantics).error— any error thrown by the initial check (network failure, etc).login(options?)— callsclient.loginWithRedirect(options). Pass{ appState: { returnTo: "/dashboard" } }to round-trip the pre-login location.logout(options?)— callsclient.logout(options)and bumps the provider'sauthVersionso 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:
- No imperative wiring — hooks return the current state, not a callback you have to subscribe and clean up.
- Re-renders on transitions — the provider tracks an internal
authVersionand bumps it on callback completion andlogout(), so components stay in sync without a separate state-management layer. - Stable references —
login/logoutare 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.