Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

React Context for Authentication State

Hello! Welcome back to our course.

In our last lesson, we built a robust server-side authentication system. We learned how to manage access and refresh tokens securely using HttpOnly cookies and server-side logic, effectively creating a persistent and secure session managed entirely by the server.

This leaves us with a critical question: The server knows the user is authenticated, but how does our client-side React UI know? How can we display the user's name or show a "Logout" button instead of a "Login" button?

Today, we'll bridge that gap. This lesson addresses the following learning outcome:

Create a React context provider to bridge authentication state from the server to the client.

We will construct a clean, type-safe, and SSR-friendly mechanism to pass the authentication status determined on the server down to our client-side components, enabling a dynamic and responsive user interface.

1. The SSR Context Challenge

In a traditional Client-Side Rendered (CSR) React application, you might store an authentication token in memory and wrap your app in a React Context provider. This is straightforward because the entire application lives and runs in the browser.

In an SSR application like one built with Tanstack Start, this is more complex. Your root layout component (__root.tsx) is often a server component, rendered on the server. React Context, however, is fundamentally a client-side feature designed to manage state and re-renders within the browser. Attempting to create a context provider directly within a server component will fail.

This video, while for Next.js, perfectly illustrates this exact problem and the standard architectural solution. The principle is identical in Tanstack Start.

Next.js 15 Tutorial - 62 - Context Providers

This video from Codevolution explains why React Context cannot be used directly in server components and demonstrates the correct pattern: creating a dedicated client component to act as the provider.

Please watch the context implementation from the beginning, and then skip ahead to the summary. Focus on: The error that occurs when createContext is used in a server component. The solution: creating a separate component with the "use client" directive. How this client provider component wraps the children within the main server layout.

The key takeaway is that we need to create a boundary. The server component will render a special client component whose sole job is to provide the context to the rest of the application.

2. The Server-to-Client State Bridge Pattern

So, how do we get our server-side session data into this client-side context provider? We'll use Tanstack Router's capabilities to create a "bridge":

  1. Server-Side Load: In our root route (src/routes/__root.tsx), we'll use the beforeLoad lifecycle hook. This function runs on the server for the initial page load. Inside it, we'll read the session data from our secure cookie.
  2. Inject into Router Context: The data returned from beforeLoad is automatically injected into the router's own context, making it available to the route's component and all child routes.
  3. Pass as Prop: The root route's component will read this data from the router context and pass it as an initial state prop to our client-side AuthProvider component.
  4. Initialize Client Context: The AuthProvider receives this initial state and uses it to initialize the React Context, making it available to any client component that needs it.

This diagram shows the flow of authentication data. The server reads the session cookie, injects the data into the router context via beforeLoad, which is then passed as a prop to a client-side AuthProvider. This provider makes the auth state available to all client components via a useAuth hook.

This pattern is beautifully demonstrated in the following article, which shows how to use beforeLoad to fetch and provide context to child routes.

Tanstack Start - A deep dive into THE React Framework

This deep dive into Tanstack Start by Nikos CS shows a practical example of using beforeLoad on the root route to fetch authentication context from the server.

Please read the middleware section (under '6.2 Router “Middleware”'). Focus on the code example for root.tsx. Notice how beforeLoad is an async function that fetches authContext and returns it. This is the server-side loading step of our pattern.

3. Implementation Walkthrough

Let's translate this pattern into code.

Step 1: Define the Authentication Context Type

First, let's define the shape of the data we want to share. This can be placed in a shared types file, like src/types/auth.ts.

// src/types/auth.ts
export type AuthContextType = {
  isAuthenticated: boolean;
  user?: {
    id: string;
    email: string;
  };
};

Step 2: Load Session Data in the Root Route

Now, we'll edit src/routes/__root.tsx to load the session data on the server.

// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
import { getAppSession } from '~/lib/session'; // Your server-side session utility
import type { AuthContextType } from '~/types/auth';
import { AuthProvider } from '~/components/AuthProvider'; // We will create this next

// Define the shape of the router's context
interface MyRouterContext {
  auth: AuthContextType;
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  // This function runs on the server during the initial load
  beforeLoad: async () => {
    const session = await getAppSession();
    const sessionData = await session.get();

    if (sessionData?.status === 'authenticated') {
      return {
        auth: {
          isAuthenticated: true,
          user: sessionData.user,
        },
      };
    }

    return {
      auth: {
        isAuthenticated: false,
      },
    };
  },
  component: RootComponent,
});

function RootComponent() {
  // Get the 'auth' object we returned from beforeLoad
  const { auth } = Route.useRouteContext();

  return (
    <html lang="en">
      <head>
        {/* ... head elements */}
      </head>
      <body>
        {/* Pass the server-loaded auth state to our client provider */}
        <AuthProvider initialAuth={auth}>
          {/* Your header/nav can now use the auth context */}
          <Header />
          <hr />
          <Outlet />
        </AuthProvider>
        <TanStackRouterDevtools />
      </body>
    </html>
  );
}

Step 3: Create the Client-Side AuthProvider

This is the client-side piece of the puzzle. Create a new file src/components/AuthProvider.tsx.

// src/components/AuthProvider.tsx
'use client'; // This is the crucial directive!

import { createContext, useContext, type ReactNode } from 'react';
import type { AuthContextType } from '~/types/auth';

// Create the React Context with a default value
const AuthContext = createContext<AuthContextType | null>(null);

// The provider component
export function AuthProvider({
  children,
  initialAuth,
}: {
  children: ReactNode;
  initialAuth: AuthContextType;
}) {
  // The value of the context is the initial state passed from the server
  return (
    <AuthContext.Provider value={initialAuth}>{children}</AuthContext.Provider>
  );
}

// A custom hook for easy consumption
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

Step 4: Consume the Context in a Client Component

Now, any client component wrapped by AuthProvider can access the authentication state. For example, a Header component can conditionally render links.

// src/components/Header.tsx
'use client';

import { Link } from '@tanstack/react-router';
import { useAuth } from './AuthProvider';

export function Header() {
  const { isAuthenticated, user } = useAuth();

  return (
    <div className="p-2 flex gap-2">
      <Link to="/" className="[&.active]:font-bold">
        Home
      </Link>
      {isAuthenticated ? (
        <>
          <span>Hello, {user?.email}!</span>
          <a href="/api/logout">Logout</a> {/* Assuming a server-side logout route */}
        </>
      ) : (
        <a href="/api/login">Login</a> {/* Assuming a server-side login route */}
      )}
    </div>
  );
}

With this setup, the server renders the initial HTML with the correct state (e.g., showing "Hello, user!"), and the client-side React application hydrates with the same state available in its context, preventing any UI flicker or mismatch.

Conclusion

You have successfully bridged your server-side authentication state to your client-side React components. This pattern is fundamental to building modern, interactive SSR applications.

Key Takeaways:

  • SSR Context Requires a Boundary: You cannot use React Context directly in server components. The solution is to create a dedicated provider component marked with "use client".
  • The Bridge Pattern: The flow is Server Load (beforeLoad) -> Router Context -> Prop -> Client Provider. This reliably transfers initial state from server to client.
  • beforeLoad is the Key: Tanstack Router's beforeLoad hook on the root route is the perfect entry point for loading session data on the server for every page request.
  • useAuth Hook: Creating a custom hook (useAuth) provides a clean, type-safe, and reusable way for client components to consume the authentication state.

Next Up:

We now have authentication state available on both the server and the client. The next logical step is to use this server-side state to protect entire routes from unauthorized access. In the next lesson, we will implement protected routes using server-side authentication checks in route loaders or middleware.

Can't find a good explanation? Sign up and we'll make it for you

Sign up