Non Blocking Rendering
This section explains how to configure your application so it can begin rendering before the user’s authentication state is fully determined.
With this setup, the initial UI appears immediately, and authentication-aware components are rendered a moment later once the auth state is resolved. The result looks like this video:
Default: blocking rendering (simplest)
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.
You just need to make sure to at least set the background color early to avoid white flashes.
Faster first paint: non-blocking rendering
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():
First, you need to remove the root OidcInitializationGate:
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router";
import { App } from "./App";
-import { OidcInitializationGate } from "~/oidc";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
- <OidcInitializationGate>
<BrowserRouter>
<App />
</BrowserRouter>
- </OidcInitializationGate>
</React.StrictMode>
);Then wrap all the components that call the useOidc() hook without assertion, into <OidcInitializationGate /> or <Suspense />:
Don't forget <AutoLogoutWarningOverlay />! If you forget to wrap a single component that call useOidc(), you're all app will suspend.
The components that call useOidc({ assert: "..." }) do not need to be wrapped into OidcInitializationGate! If you are able to make an assertion, the auth state has been established already and those calls will never suspend!
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.
Only if you are using withLoginEnforced()
withLoginEnforced()Consider this:
import { withLoginEnforced } from "~/oidc";
// This component can suspend when rendered (like a lazy component would)
// You must define a suspense boundary around it (or use OidcInitializationGate).
const Protected = withLoginEnforced(() => {
return <div>{/* ... */}</div>;
});
export default Protected;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:
TL;DR
<OidcInitializationGate />at the root: simpler mental model, no layout shift.<Suspense />or<OidcInitializationGate />nearuseOidc()calls: faster perceived load, better user experience.Components using
useOidc({ assert: "..." })do not need to be wrapped, they will never suspend.If you use
withLoginEnforced()it need to be wrapped as well.Don't forget to wrap
AutoLogoutWarningOverlay
(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 the default, since it's required for server rendering. However, if you find the layout shift caused by auth-aware components appearing after hydration annoying to handle, you can easily delay rendering your app until the OIDC initialization process has completed:
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
import Header from "@/components/Header";
import { AutoLogoutWarningOverlay } from "@/components/AutoLogoutWarningOverlay";
import appCss from "../styles.css?url";
import { useOidc } from "@/oidc";
export const Route = createRootRoute({
head: () => ({ /* ... */ }),
shellComponent: RootDocument
});
function RootDocument({ children }: { children: React.ReactNode }) {
const { isOidcReady } = useOidc();
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body
className="min-h-screen text-white"
style={{
backgroundColor: "#0f172a",
backgroundImage: "linear-gradient(180deg, #0f172a 0%, #1e293b 50%, #0f172a 100%)"
}}
>
<div className="min-h-screen flex flex-col">
{isOidcReady && (
<>
<Header />
<main className="flex flex-1 flex-col">
<div className="flex flex-1 flex-col">{children}</div>
</main>
</>
)}
</div>
<AutoLogoutWarningOverlay />
<Scripts />
</body>
</html>
);
}Then, you don't need to test anymore if oidc is ready:
function AuthButtons(props: { className?: string }) {
const { className } = props;
- const { isOidcReady, isUserLoggedIn } = useOidc();
+ const { isUserLoggedIn } = useOidc({ assert: "ready" });
- if (!isOidcReady) {
- return null;
- }
return (
<div className={["opacity-0 animate-[fadeIn_0.2s_ease-in_forwards]", className].join(" ")}>
{isUserLoggedIn ? <LoggedInAuthButton /> : <NotLoggedInAuthButton />}
</div>
);
}
function Greeting() {
- const { isOidcReady, isUserLoggedIn, decodedIdToken } = useOidc();
+ const { isOidcReady, isUserLoggedIn, decodedIdToken } = useOidc({ assert: "ready" });
- if (!isOidcReady) {
- return <> </>;
- }
return (
<span className="opacity-0 animate-[fadeIn_0.2s_ease-in_forwards]">
{isUserLoggedIn ? `Welcome back ${decodedIdToken.name}` : `Hello anonymous visitor!`}
</span>
);
}Last updated
Was this helpful?