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:

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.

  • 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():

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. (And don’t forget to wrap <AutoLogoutWarningOverlay /> as well.)


Components protected with enforceLogin

Any 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:

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:

src/pages/Protected.tsx
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 /> near useOidc() calls: faster perceived load, better user experience.

  • enforceLogin and withLoginEnforced() automatically handle suspension but Page component wrapped into withLoginEnforced() 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.)

Last updated

Was this helpful?