Non Blocking Rendering
When using the oidc-spa/react-spa adapter, the recommended setup is to wrap your entire application in an <OidcInitializationGate />, like so:
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
import { OidcInitializationGate } from "~/oidc";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<OidcInitializationGate>
<App />
</OidcInitializationGate>
</React.StrictMode>
);By default, this setup defers rendering your entire app until bootstrapOidc() has resolved, in other words, until oidc-spa has contacted your IdP and determined whether the user currently has an active session.
This is often the simplest and safest choice:
You don’t have to think about whether the auth state has settled.
There’s no risk of layout shifts.
Tests and SSR behave predictably (SSR is canceled).
However, for optimal performance, you can start rendering before the authentication state is resolved, letting the page appear instantly, while auth-aware components hydrate a few milliseconds later.
For example:
In this short demo, the homepage renders immediately, and components depending on authentication appear shortly after the session check completes.
You can achieve this simply by moving <OidcInitializationGate /> closer to the components that call useOidc():
import { Suspense } from "react";
import {
useOidc,
OidcInitializationGate
} from "~/oidc";
export function Header() {
return (
<header>
{/* ... */}
<OidcInitializationGate fallback={<Spinner />}>
<AuthButtons />
</OidcInitializationGate>
{/* OR */}
{/*
<Suspense fallback={<Spinner />}>
<AuthButtons />
</Suspense>
*/}
</header>
);
}
function AuthButtons() {
const { isUserLoggedIn } = useOidc();
return (
<div className="animate-fade-in">
{isUserLoggedIn ? <LoggedInAuthButtons /> : <NotLoggedInAuthButtons />}
</div>
);
}Using React’s built-in Suspense
You can use React’s built-in <Suspense /> instead of <OidcInitializationGate />.
This is often even better, as it lets you define a unified fallback for all your app’s asynchronous operations.
When called before the auth state is ready, useOidc() throws a Promise, which React will catch using the nearest Suspense boundary.
This means you must wrap any component that calls useOidc() in either <OidcInitializationGate /> or <Suspense />.
If you don’t, your entire app will suspend.
(And don’t forget to wrap <AutoLogoutWarningOverlay /> as well.)
Components protected with enforceLogin
enforceLoginAny component that’s behind enforceLogin() or wrapped in a withLoginEnforced() component will not suspend, because those act as their own authentication gates.
However, note that if you use withLoginEnforced() directly, the resulting component can still suspend, so you want to wrap them too.
Example:
import { lazy, Suspense } from "react";
import { Navigate, Route, Routes } from "react-router";
import { AutoLogoutWarningOverlay } from "./components/AutoLogoutWarningOverlay";
import { Header } from "./components/Header";
import { Home } from "./pages/Home";
const Protected = lazy(() => import("./pages/Protected"));
const AdminOnly = lazy(() => import("./pages/AdminOnly"));
export function App() {
return (
<>
<Header />
<main>
<Suspense fallback={<Spinner />}>
<Routes>
<Route index element={<Home />} />
<Route path="protected" element={<Protected />} />
<Route path="admin-only" element={<AdminOnly />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</main>
<Suspense>
<AutoLogoutWarningOverlay />
</Suspense>
</>
);
}With route components like:
import { withLoginEnforced } from "~/oidc";
const Protected = withLoginEnforced(() => {
return <div>{/* ... */}</div>;
});
export default Protected;TL;DR
<OidcInitializationGate />at the root: simpler mental model, no layout shift.<Suspense />or<OidcInitializationGate />nearuseOidc()calls: faster perceived load, better user experience.enforceLoginandwithLoginEnforced()automatically handle suspension but Page component wrapped intowithLoginEnforced()do suspend themselvs.Both options are supported choose based on your desired UX and simplicity.
(In modern browsers, session restoration typically takes under 300 ms, so even full gating often feels instant.)
Default: blocking rendering (simplest and safest)
When using the oidc-spa/angular adapter, the recommended default is to let bootstrap wait for OIDC.
You do this by using your Oidc service and not opting out of provider waiting (the default).
app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { Oidc } from './services/oidc.service';
export const appConfig: ApplicationConfig = {
providers: [
// This will NOT resolve until bootstrapOidc() completes.
Oidc.provide({
// ...
}),
provideRouter(routes),
],
};Oidc service (simple example)
import { Injectable } from '@angular/core';
import { AbstractOidcService } from 'oidc-spa/angular';
export type DecodedIdToken = {
name: string;
realm_access?: { roles: string[] };
};
@Injectable({ providedIn: 'root' })
export class Oidc extends AbstractOidcService<DecodedIdToken> {
// providerAwaitsInitialization defaults to true
}With this setup, Angular only renders once bootstrapOidc() has completed (the IdP has been contacted and the session state is known).
Why this is nice
You do not think about “is OIDC ready”.
No layout shifts.
Tests and SSR behave predictably. (NOTE: SSR in Angular not tested yet)
Faster first paint: non-blocking rendering
For optimal performance, you can start rendering before the authentication state is fully resolved, so the page appears instantly and OIDC-aware parts “hydrate” moments later.
Example of what it can look in action:
Enable this by opting out of provider waiting in your Oidc service:
// examples/angular-kitchensink/src/app/services/oidc.service.ts
import { Injectable } from '@angular/core';
import { AbstractOidcService } from 'oidc-spa/angular';
@Injectable({ providedIn: 'root' })
export class Oidc extends AbstractOidcService {
// The provider no longer blocks Angular bootstrap
override providerAwaitsInitialization = false;
// ...
}Important: Once you do this, you are responsible for placing “init boundaries” in templates, so parts of the UI that need OIDC only render once it is ready.
Gate OIDC-aware UI with @defer
@deferUse Angular’s built-in @defer with a @placeholder for instant paint:
<!-- examples/angular-kitchensink/src/app/app.html -->
<header>
<span>OIDC-SPA + Angular (Kitchen Sink)</span>
@defer (when oidc.prInitialized | async) {
<!-- Safe to read OIDC values here -->
@if (oidc.isUserLoggedIn) {
<div>
<span>Hello {{ oidc.$decodedIdToken().name }}</span>
<button (click)="oidc.logout({ redirectTo: 'home' })">Logout</button>
</div>
} @else {
<div>
<button (click)="oidc.login()">Login</button>
<button (click)="
oidc.login({
transformUrlBeforeRedirect: keycloakUtils.transformUrlBeforeRedirectForRegister,
})
">
Register
</button>
</div>
}
} @placeholder {
<span style="line-height: 1.35;">Initializing OIDC...</span>
}
</header>Anywhere you read things like oidc.isUserLoggedIn, oidc.$decodedIdToken(), or values derived from issuerUri, put them behind a @defer (when oidc.prInitialized | async) (or otherwise guard them) to avoid runtime errors during the brief initialization window.
Access helpers lazily to avoid crashes
Because the component can be constructed before OIDC is initialized, compute helpers like keycloakUtils lazily:
import { Component, inject } from '@angular/core';
import { Oidc } from './services/oidc.service';
import { createKeycloakUtils } from 'oidc-spa/keycloak';
@Component({
selector: 'app-root',
templateUrl: './app.html',
imports: [],
})
export class App {
oidc = inject(Oidc);
// Use a getter so we read issuerUri only after init
get keycloakUtils() {
return createKeycloakUtils({ issuerUri: this.oidc.issuerUri });
}
// Example: drive an "Admin only" link state
get canShowAdminLink(): boolean {
if (!this.oidc.isUserLoggedIn) return true;
const roles = this.oidc.$decodedIdToken().realm_access?.roles ?? [];
return roles.includes('admin');
}
}TL;DR
Blocking at bootstrap (default):
Oidc.provide()waits forbootstrapOidc()before Angular renders. Easiest mental model. No layout shift. Tests and SSR are straightforward.Non-blocking: set
override providerAwaitsInitialization = falsein yourOidcservice. Then:Gate auth-aware UI with
@defer (when oidc.prInitialized | async) { ... } @placeholder { ... }.Access helpers like
keycloakUtilsvia a getter so you do not touchissuerUribefore init.Guard overlays or any code that reads OIDC state.
Choose based on the UX you want. Both modes are supported.
(In modern browsers, session restoration usually completes in under ~300 ms, so even full gating often feels instant.)
In TanStack Start, non blocking rendering is enabled by default! It's a condition to levrage SSR at least on marketing pages.
Last updated
Was this helpful?