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)

src/main.tsx
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:

src/main.tsx
 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 />:

src/components/Header.tsx
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()

Consider this:

src/pages/Protected.tsx
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:

src/App.tsx
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 /> near useOidc() 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.)

Last updated

Was this helpful?