Guards
Components and HOCs that protect routes and UI.
The React layer ships five guard components and two HOCs. They're thin — under the hood they all call useAuthStatus, useHasRole, or useHasPermission and decide what to render.
<AuthGuard>
Renders children only when the user is authenticated.
| Prop | Default |
|---|---|
loadingFallback | null (renders nothing while auth is initializing) |
fallback | null (renders nothing when unauthenticated) |
onUnauthenticated | (none) |
<GuestGuard>
The opposite of <AuthGuard>. Renders children only when the user is not authenticated. Use on /login, /signup to redirect already-logged-in users away.
| Prop | Type | Notes |
|---|---|---|
loadingFallback | ReactNode? | While auth state loads |
fallback | ReactNode? | Rendered when authenticated (and no onAuthenticated) |
onAuthenticated | () => void? | Called when authenticated — use for navigation |
allowWhenAddingAccount | boolean? | Render children even when authenticated (default false) |
Adding another account: set
allowWhenAddingAccountto render the login form to a user who is already signed in — the Gmail-style "Add another account" flow. With it on, theonAuthenticatedredirect andfallbackare skipped, so the login form shows. Drive it from your own signal (e.g. a?add=1query param). For a ready-made wrapper, prefer<AddAccountGuard>.
<AddAccountGuard>
A GuestGuard that also renders its children while an already-authenticated user is adding another account. A plain <GuestGuard> redirects every authenticated user away from the login form — which makes "Add another account" impossible, since the user is authenticated by definition. <AddAccountGuard> renders the login form when the app is in add-account mode, and otherwise behaves exactly like <GuestGuard> (redirect/fallback when authenticated).
AddAccountGuardProps extends GuestGuardProps (minus allowWhenAddingAccount) and adds:
| Prop | Type | Notes |
|---|---|---|
adding | boolean? | Whether the app is in add-account mode (default true). When true, the login form renders even if a user is already signed in. |
Pair this with the switcher's
addAccountfromuseAccountSwitcher. See the Multi-account login & switching recipe.
<RequireRole>
Render children only if the user has the required role(s).
| Prop | Type | Notes |
|---|---|---|
role | string | string[] | Required role(s) |
matchAll | boolean | Require ALL (default: ANY) |
loadingFallback | ReactNode? | While auth state loads |
fallback | ReactNode? | When the user lacks the role(s) |
onAccessDenied | () => void? | Called when fallback renders |
<RequirePermission>
Same shape as <RequireRole> but for permissions.
HOCs
For class components or higher-order composition:
withRequireRole(Component, options)
The wrapped component receives extra props: { hasRole, isLoading, isAuthenticated }.
withRequirePermission(Component, options)
Same idea, for permissions.
Factory builders
If you reuse the same role/permission policy in many places, build a partially-applied HOC:
Same pattern for createRequirePermissionHOC.
Combining guards
Nest them — they compose cleanly:
For very common combos (auth + role), wrap in a custom component to keep your route file tidy.
Guard renders a blank page for a signed-in user
If a guard renders nothing for a user you know is authenticated, the usual cause is @ackplus/nest-auth-react being installed twice — common in pnpm/monorepos when a peer-React version split double-installs it. Each copy used to create its own React context, so <AuthProvider> from one copy populated one context while the hooks inside your guard read a different, still-default one: isLoading never flipped to false, and the guard sat rendering its (absent) loading UI forever.
Since 2.7.3 the contexts are cross-realm singletons pinned on globalThis, so duplicate copies share one context and the app works anyway. A guard that renders nothing purely because auth is still loading also emits a one-time dev-only console.warn pointing at this cause (production builds stay silent).
The real fix is still to dedupe the package to a single copy:
Then align the react / react-dom peer ranges (or add a workspace overrides / resolutions entry) so only one copy is installed. Passing an explicit loadingComponent to your guards also makes a stuck-loading state visible instead of blank.
Related
- Hooks.
- RBAC.
- Next.js dashboard recipe — App-Router-style guards.