@userspace-auth/react
v1.10.7
Published
React SDK for Userspace Auth, session state, tokens, and auth guards
Downloads
1,495
Maintainers
Readme
Userspace React SDK (@userspace-auth/react)
Session state, tokens, and auth guards for a React app backed by Userspace.
Sign-in itself happens on your Userspace deployment's hosted pages. This package tells your app who is signed in and hands you a token to call your own backend with.
Install
npm i @userspace-auth/reactRequires React 18 or 19, declared as peer dependencies. The package has no runtime dependencies of its own.
ESM only. It works in Vite, Next.js, Remix, Parcel, esbuild, webpack 5, and anything
else that resolves import. A CommonJS-only setup, for example an older Jest config
without ESM support, cannot require() it.
Quick start
Wrap your app once:
import { UserspaceProvider } from '@userspace-auth/react'
export function App() {
return (
<UserspaceProvider baseUrl="https://acme.userspace.tech" clientId="your-client-id">
<Dashboard />
</UserspaceProvider>
)
}Then read the session anywhere beneath it:
import { useSession, SignedIn, SignedOut } from '@userspace-auth/react'
function Dashboard() {
const { session, isLoading } = useSession()
if (isLoading) return <p>Loading…</p>
return (
<>
<SignedIn>
<p>Signed in as {session?.identity?.email}</p>
</SignedIn>
<SignedOut>
<a href="https://acme.userspace.tech/sign-in">Sign in</a>
</SignedOut>
</>
)
}Calling your backend
getToken() returns a JWT your backend can verify locally. Send it as a bearer token:
function Profile() {
const { getToken } = useSession()
const load = async () => {
const token = await getToken()
if (!token) return // signed out
const res = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${token}` },
})
return res.json()
}
// …
}From your api layer, outside React
If your data layer is a plain module of functions over one shared fetcher, import
getToken directly instead of threading a hook through it. It is the same function
useSession() returns, so the two cannot drift:
import { getToken } from '@userspace-auth/react'
async function authorized(path: string, init: RequestInit = {}) {
const token = await getToken()
const headers = new Headers(init.headers)
if (token) headers.set('Authorization', `Bearer ${token}`)
return fetch(`https://api.example.com${path}`, { ...init, headers })
}
export const fetchUsers = () => authorized('/users').then((r) => r.json())Called before <UserspaceProvider> has rendered, it waits for the provider to configure
the SDK rather than firing at a half-configured base URL. The provider stays the only
place configuration happens. If the provider never mounts, the call warns after a few
seconds instead of hanging silently.
On the backend, verify it with the Userspace SDK for your stack, for Go that is
github.com/userspace-auth/sdk-go. Any JOSE library works too: the token is a standard
RS256 JWT signed by keys published at https://<your-deployment>/.well-known/jwks.json.
Never send this token anywhere but your own backend. It also authenticates against the Userspace API as that user.
API
<UserspaceProvider>
| Prop | Type | Meaning |
| --------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| baseUrl | string | Your deployment's base URL, e.g. https://acme.userspace.tech. |
| clientId | string | Your client ID. The same one your backend is configured with. |
| webcomponents | boolean | Registers the x-* custom elements (<x-signed-in>, <x-trigger>, ...) so you can use them in JSX out of the box. Off by default. |
| preload | boolean | Warms the settings popup bundle at browser idle time, so the first openSettings() opens without a download. Off by default. |
| autoLogin | boolean | Redirects signed-out visitors to the hosted sign-in page, with redirect_uri pointing back at the current URL. Off by default. |
| children | ReactNode | Your app. |
Must wrap every component that uses this package. Hooks throw a clear error outside it rather than returning an empty session.
webcomponents loads the element bundle lazily: with the flag off, none of it is
downloaded. The tags are typed for TSX either way.
autoLogin probes the session once on mount, sharing the request useSession() makes,
and only redirects when the probe comes back signed out. A transport error is an unknown
state, not a signed-out one, and never redirects. Use it for apps with no public pages;
apps with a signed-out state should render <SignedOut> instead.
useSession()
const { session, getToken, isLoading, error } = useSession()| Field | Type | Meaning |
| ----------- | ------------------------------- | ----------------------------------------------------------------------------------------------- |
| session | SessionData \| null | The session, or null while loading or signed out. |
| getToken | () => Promise<string \| null> | A JWT for your backend, or null if signed out. Rejects on transport errors. |
| isLoading | boolean | True until the first load settles. |
| error | unknown | A real failure. Being signed out is not an error, it is session: null with error: null. |
session.identity.id is the stable user id. Key your own data on it, never on
session.session.id, which changes on every sign-in.
getToken()
import { getToken } from '@userspace-auth/react'() => Promise<string | null>, the same function useSession() returns, importable
without a hook so a module-scope fetcher can use it. Resolves null when signed out,
rejects on transport errors, and waits for <UserspaceProvider> to configure the SDK if
it is called first. Client-side only, see Next.js for why.
openSettings()
import { openSettings } from '@userspace-auth/react'
;<MenuItem onClick={() => openSettings()}>Account settings</MenuItem>Opens the Userspace settings popup from your own components — a profile menu item,
a toolbar button, anything clickable. One popup instance serves every caller. Works
with or without the webcomponents flag: the popup code loads lazily on first call.
Pass preload to the provider to move that download (and the SDK's config load) to
idle time, so the first click opens instantly. The popup opens immediately and loads
its content in place; without an active session it closes again with a console
warning, so keep the control inside <SignedIn>.
signIn()
import { signIn } from '@userspace-auth/react'
;<Button onClick={() => signIn()}>Sign in</Button>Leaves for the hosted sign-in page, carrying redirect_uri back to the current
URL so the user returns where they were after signing in. The imperative
counterpart of the autoLogin flag: same navigation, on a click instead of on
every signed-out visit. Waits for <UserspaceProvider> to configure the SDK if
it is called first, like getToken().
signOut()
import { signOut } from '@userspace-auth/react'
;<MenuItem onClick={() => signOut()}>Sign out</MenuItem>Signs the user out and navigates to the hosted sign-in page — or, with
signOut({ redirectUri }), to an absolute URL whose origin is on the tenant's
allowlist; the server validates it and untrusted targets fall back to sign-in.
Sign-out ends every session in this browser, not only the current one: the server
terminates the whole cookie-scoped session, so other tabs sign out too. Waits for
<UserspaceProvider> to configure the SDK if it is called first, like getToken().
<SignedIn> / <SignedOut>
Render their children only in the matching state, and nothing at all while loading, so
neither flashes before the session is known. An errored session counts as signed out:
SignedIn never renders on an unknown state.
Types
SessionData, SessionInfo, UserspaceUser, EmailAddress, UseSessionResult,
UserspaceProviderProps, GuardProps, and UserspaceTagName are exported. The package ships its own
declarations, so strict: true works with no @types/* package and no any.
Caching behaviour
The session is fetched once per page load and held. Every component calling
useSession() shares that one request, including several mounting at once on a cold
cache.
Deliberately not included: revalidation on window focus or reconnect, polling, and manual mutation. If you need the session re-read after an action that changes it, reload the page or lift that state into your own store. This is a smaller contract than a general data-fetching library on purpose, it keeps the package free of runtime dependencies, which is why installing it cannot conflict with whatever data layer you already use.
Next.js
The bundle carries a 'use client' directive, so importing it from an App Router server
component works without your adding one. Components using useSession are client
components like any other hook consumer.
Server rendering is unaffected: client components are prerendered and hydrated as usual, and this package renders a defined loading state on the server.
What the directive does rule out is calling getToken from a server component, a server
action, or a route handler, and that is deliberate rather than an oversight. The token
cache and the configured base URL are module singletons, so in a long-lived server
process one request's token would sit in the same cache the next request reads. Serving
tokens server-side needs per-request identity threaded through the call, which is a
different contract, not a second build of this one. Use this package from the client, and
have your own server read whatever session your backend already trusts.
License
MIT, see LICENSE.
