Auth for React
OpenID Connect authentication SDK for React single-page applications.
Installation
npm install @usetransactional/auth-reactpnpm add @usetransactional/auth-reactyarn add @usetransactional/auth-reactQuick Start
Wrap your application in the TransactionalAuthProvider and use the useAuth hook to access authentication state.
import { TransactionalAuthProvider, useAuth } from "@usetransactional/auth-react";
function App() {
return (
<TransactionalAuthProvider
domain="auth.yourdomain.com"
clientId="your-client-id"
redirectUri={window.location.origin}
>
<LoginButton />
</TransactionalAuthProvider>
);
}
function LoginButton() {
const { isAuthenticated, isLoading, loginWithRedirect, logout, user } = useAuth();
if (isLoading) return <div>Loading...</div>;
if (isAuthenticated) {
return (
<div>
<p>Welcome, {user?.name}</p>
<button onClick={() => logout()}>Log out</button>
</div>
);
}
return <button onClick={() => loginWithRedirect()}>Log in</button>;
}Provider
The TransactionalAuthProvider wraps your application and manages all authentication state, token storage, and silent renewal.
import { TransactionalAuthProvider } from "@usetransactional/auth-react";
function App() {
return (
<TransactionalAuthProvider
domain="auth.yourdomain.com"
clientId="your-client-id"
redirectUri={window.location.origin}
scope="openid profile email"
audience="https://api.yourdomain.com"
cacheLocation="localstorage"
useRefreshTokens={true}
onRedirectCallback={(appState) => {
window.history.replaceState(
{},
document.title,
appState?.returnTo || window.location.pathname
);
}}
>
{children}
</TransactionalAuthProvider>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
domain | string | required | Your Transactional Auth domain |
clientId | string | required | Your application's client ID |
redirectUri | string | window.location.origin | URL to redirect to after login |
scope | string | "openid profile email" | OAuth scopes to request |
audience | string | — | API audience identifier |
cacheLocation | "memory" | "localstorage" | "localstorage" | Where to store tokens |
useRefreshTokens | boolean | true | Use refresh tokens for silent renewal |
onRedirectCallback | (appState?) => void | — | Called after the redirect login completes |
Hooks
useAuth
The primary hook for accessing all authentication state and methods.
import { useAuth } from "@usetransactional/auth-react";
function Dashboard() {
const {
isAuthenticated,
isLoading,
user,
error,
loginWithRedirect,
loginWithPopup,
logout,
getAccessToken,
getIdTokenClaims,
hasPermission,
hasRole,
} = useAuth();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<p>Authenticated: {isAuthenticated ? "Yes" : "No"}</p>
{user && <p>Email: {user.email}</p>}
{hasPermission("emails:send") && <p>You can send emails</p>}
{hasRole("admin") && <p>You are an admin</p>}
</div>
);
}Returns:
| Property | Type | Description |
|---|---|---|
isAuthenticated | boolean | Whether the user is logged in |
isLoading | boolean | Whether the auth state is still loading |
user | User | undefined | The authenticated user's profile |
error | Error | undefined | Any authentication error |
loginWithRedirect | (options?) => Promise<void> | Redirect to the login page |
loginWithPopup | (options?) => Promise<void> | Open login in a popup window |
logout | (options?) => void | Log the user out |
getAccessToken | () => Promise<string> | Get the current access token (refreshes if expired) |
getIdTokenClaims | () => Promise<IdTokenClaims> | Get the decoded ID token claims |
hasPermission | (permission: string) => boolean | Check if the user has a specific permission |
hasRole | (role: string) => boolean | Check if the user has a specific role |
useUser
A convenience hook that returns just the user object and loading state.
import { useUser } from "@usetransactional/auth-react";
function UserProfile() {
const { user, isLoading } = useUser();
if (isLoading) return <div>Loading profile...</div>;
if (!user) return <div>Not logged in</div>;
return (
<div>
<img src={user.picture} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}useAccessToken
Returns the current access token or null if the user is not authenticated. The token is kept fresh automatically.
import { useAccessToken } from "@usetransactional/auth-react";
function ApiStatus() {
const token = useAccessToken();
return (
<div>
<p>Token available: {token ? "Yes" : "No"}</p>
</div>
);
}Components
AuthGuard
Protects its children from being rendered unless the user is authenticated. Displays a fallback while loading and optionally redirects unauthenticated users to the login page.
import { AuthGuard } from "@usetransactional/auth-react";
function ProtectedPage() {
return (
<AuthGuard
fallback={<div>Loading...</div>}
onUnauthenticated={() => console.log("User not authenticated")}
autoRedirect={true}
>
<h1>Protected Content</h1>
<p>Only visible to authenticated users.</p>
</AuthGuard>
);
}Props:
| Prop | Type | Default | Description |
|---|---|---|---|
fallback | ReactNode | — | Shown while auth state is loading |
onUnauthenticated | () => void | — | Called when the user is not authenticated |
autoRedirect | boolean | true | Automatically redirect to login if not authenticated |
withAuthenticationRequired
A higher-order component (HOC) that wraps a component and ensures the user is authenticated before rendering it.
import { withAuthenticationRequired } from "@usetransactional/auth-react";
function SettingsPage() {
return <h1>Account Settings</h1>;
}
export default withAuthenticationRequired(SettingsPage, {
fallback: <div>Redirecting to login...</div>,
onUnauthenticated: () => console.log("Redirecting..."),
returnTo: "/settings",
});Options:
| Option | Type | Default | Description |
|---|---|---|---|
fallback | ReactNode | — | Shown while auth state is loading |
onUnauthenticated | () => void | — | Called when redirect happens |
returnTo | string | — | URL to return to after login |
Login and Logout Options
loginWithRedirect
Redirects the user to the Transactional Auth login page.
const { loginWithRedirect } = useAuth();
// Basic redirect
await loginWithRedirect();
// With options
await loginWithRedirect({
returnTo: "/dashboard",
appState: { targetUrl: "/onboarding" },
connection: "google-oauth2", // skip to a specific identity provider
loginHint: "user@example.com", // pre-fill the email field
});Options:
| Option | Type | Description |
|---|---|---|
returnTo | string | URL to navigate to after login |
appState | object | Custom state passed through the redirect |
connection | string | Identity provider connection name |
loginHint | string | Pre-fill the email/username field |
loginWithPopup
Opens the login page in a popup window instead of redirecting.
const { loginWithPopup } = useAuth();
// Basic popup
await loginWithPopup();
// With options
await loginWithPopup({
returnTo: "/dashboard",
appState: { targetUrl: "/onboarding" },
connection: "google-oauth2",
loginHint: "user@example.com",
});Takes the same options as loginWithRedirect.
logout
Logs the user out and optionally redirects.
const { logout } = useAuth();
// Basic logout (redirects to current origin)
logout();
// With options
logout({
returnTo: "https://yourdomain.com",
federated: true, // also log out of the identity provider
});Options:
| Option | Type | Description |
|---|---|---|
returnTo | string | URL to redirect to after logout |
federated | boolean | Also log out from the upstream identity provider |
Making API Calls
Use getAccessToken() to retrieve a valid access token and attach it to your API requests as a bearer token.
import { useAuth } from "@usetransactional/auth-react";
import { useState, useEffect } from "react";
function EmailList() {
const { getAccessToken, isAuthenticated } = useAuth();
const [emails, setEmails] = useState([]);
useEffect(() => {
if (!isAuthenticated) return;
async function fetchEmails() {
const token = await getAccessToken();
const response = await fetch("https://api.yourdomain.com/emails", {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
if (response.ok) {
const data = await response.json();
setEmails(data.emails);
}
}
fetchEmails();
}, [isAuthenticated, getAccessToken]);
return (
<ul>
{emails.map((email) => (
<li key={email.id}>{email.subject}</li>
))}
</ul>
);
}Creating a Reusable API Client
import { useAuth } from "@usetransactional/auth-react";
import { useCallback } from "react";
function useApiClient() {
const { getAccessToken } = useAuth();
const apiFetch = useCallback(
async (path: string, options: RequestInit = {}) => {
const token = await getAccessToken();
const response = await fetch(`https://api.yourdomain.com${path}`, {
...options,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
},
[getAccessToken]
);
return { apiFetch };
}
// Usage
function Dashboard() {
const { apiFetch } = useApiClient();
async function sendEmail() {
await apiFetch("/emails", {
method: "POST",
body: JSON.stringify({
to: "user@example.com",
subject: "Hello",
htmlBody: "<p>World</p>",
}),
});
}
return <button onClick={sendEmail}>Send Email</button>;
}Next.js App Router
When using the SDK with Next.js App Router, the provider must be rendered in a client component since it relies on browser APIs and React context.
Create a client wrapper component:
// app/providers.tsx
"use client";
import { TransactionalAuthProvider } from "@usetransactional/auth-react";
export function AuthProvider({ children }: { children: React.ReactNode }) {
return (
<TransactionalAuthProvider
domain="auth.yourdomain.com"
clientId="your-client-id"
redirectUri={typeof window !== "undefined" ? window.location.origin : ""}
audience="https://api.yourdomain.com"
>
{children}
</TransactionalAuthProvider>
);
}Use it in your root layout:
// app/layout.tsx
import { AuthProvider } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}Use hooks in client components:
// app/dashboard/page.tsx
"use client";
import { useAuth, AuthGuard } from "@usetransactional/auth-react";
export default function DashboardPage() {
return (
<AuthGuard fallback={<div>Loading...</div>}>
<DashboardContent />
</AuthGuard>
);
}
function DashboardContent() {
const { user } = useAuth();
return <h1>Welcome, {user?.name}</h1>;
}Full SPA Example
import {
TransactionalAuthProvider,
useAuth,
AuthGuard,
} from "@usetransactional/auth-react";
import { BrowserRouter, Routes, Route, Link, Navigate } from "react-router-dom";
// --- App Entry ---
function App() {
return (
<TransactionalAuthProvider
domain="auth.yourdomain.com"
clientId="your-client-id"
redirectUri={window.location.origin}
audience="https://api.yourdomain.com"
scope="openid profile email"
cacheLocation="localstorage"
useRefreshTokens={true}
onRedirectCallback={(appState) => {
window.history.replaceState(
{},
document.title,
appState?.returnTo || "/"
);
}}
>
<BrowserRouter>
<Navigation />
<Routes>
<Route path="/" element={<HomePage />} />
<Route
path="/dashboard"
element={
<AuthGuard fallback={<div>Loading...</div>}>
<DashboardPage />
</AuthGuard>
}
/>
<Route
path="/settings"
element={
<AuthGuard fallback={<div>Loading...</div>}>
<SettingsPage />
</AuthGuard>
}
/>
</Routes>
</BrowserRouter>
</TransactionalAuthProvider>
);
}
// --- Navigation ---
function Navigation() {
const { isAuthenticated, isLoading, loginWithRedirect, logout, user } = useAuth();
return (
<nav>
<Link to="/">Home</Link>
{isAuthenticated && <Link to="/dashboard">Dashboard</Link>}
{isAuthenticated && <Link to="/settings">Settings</Link>}
{isLoading ? (
<span>Loading...</span>
) : isAuthenticated ? (
<div>
<span>{user?.name}</span>
<button onClick={() => logout({ returnTo: window.location.origin })}>
Log out
</button>
</div>
) : (
<button onClick={() => loginWithRedirect()}>Log in</button>
)}
</nav>
);
}
// --- Pages ---
function HomePage() {
const { isAuthenticated } = useAuth();
return (
<div>
<h1>Welcome</h1>
{isAuthenticated ? (
<p>You are logged in. Visit your <Link to="/dashboard">dashboard</Link>.</p>
) : (
<p>Please log in to continue.</p>
)}
</div>
);
}
function DashboardPage() {
const { user, getAccessToken } = useAuth();
const [data, setData] = useState(null);
useEffect(() => {
async function loadData() {
const token = await getAccessToken();
const res = await fetch("https://api.yourdomain.com/dashboard", {
headers: { Authorization: `Bearer ${token}` },
});
setData(await res.json());
}
loadData();
}, [getAccessToken]);
return (
<div>
<h1>Dashboard</h1>
<p>Welcome back, {user?.name}</p>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
function SettingsPage() {
const { user, hasPermission } = useAuth();
return (
<div>
<h1>Settings</h1>
<p>Email: {user?.email}</p>
{hasPermission("settings:write") ? (
<button>Save Changes</button>
) : (
<p>You do not have permission to modify settings.</p>
)}
</div>
);
}