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

Typed Auth Context for Server & Client

Hello! Let's get started with our next lesson.

Introduction

In our last session, we successfully built the server-side "gatekeeper" for our application. We can now protect routes with beforeLoad and secure server functions with middleware by validating a JWT from an HTTP-only cookie. This ensures our backend is secure.

However, a significant gap remains: our client-side UI is completely unaware of the user's authentication status. It doesn't know whether to show a "Login" or "Logout" button, or how to greet a logged-in user.

This lesson directly addresses the learning outcome: Create a typed authentication context for use on both the server and the client. We will bridge the server-client divide, creating a single, type-safe source of truth for the user's authentication state that works seamlessly in an SSR environment.

We'll explore two powerful patterns for this:

  1. The React Context Pattern: Using standard React createContext combined with a TanStack Start useServerFn hook. This is the primary approach recommended in the official documentation.
  2. The Router Context Pattern: Leveraging a built-in feature of TanStack Router for dependency injection.

By the end of this lesson, you'll have a robust, typed context that makes the current user's data available to any component in your application, on both the server and the client.


1. The Core Problem: State Synchronization in SSR

On the server, we can determine the user's identity by reading and validating the request's cookie. On the client, we have no direct access to this secure, HTTP-only cookie.

Our goal is to:

  1. On the server (during the initial page load): Read the cookie, validate the user, and make the user's data available to the React components being rendered to HTML.
  2. On the client (after hydration): Have access to that same user data without having to re-fetch it immediately, ensuring a seamless UI without flashes of unauthenticated content.
  3. On the client (during navigation): Be able to update or refetch this data when needed (e.g., after a login or logout).

Let's explore how TanStack Start helps us solve this elegantly.

2. Approach 1: React Context with useServerFn

This is the most idiomatic approach in TanStack Start. It uses familiar React patterns (createContext, useContext) and enhances them with a powerful tool for server-client communication: useServerFn.

The strategy is to create an AuthProvider component that wraps our application. This provider will be responsible for fetching the user data and making it available via a useAuth hook.

Authentication | TanStack Start React Docs

The official TanStack Start documentation provides a concise and direct implementation of this pattern. Let's study it to understand the core components.

Read section '3. Authentication Context'. Pay close attention to the structure: the AuthContextType, the AuthProvider component, and the useAuth hook. Notice how useServerFn is the key to fetching the data.

Deconstructing the Pattern

Let's break down the code from the documentation and adapt it to our project.

Step 1: Define the Server Function

First, we need a server function that can be called from our context provider. This function will reuse the getAuthenticatedUser logic we created in the previous lesson.

// src/server/auth.ts
import 'server-only';
import { server$ } from '@tanstack/react-start/server';
import { getCookie } from 'vinxi/http';
import jwt from 'jsonwebtoken';

// Assuming UserPayload is defined as before
interface UserPayload {
  userId: string;
  email: string; // Let's use email from your previous example
  role: string;
}

// Re-using our validation logic from the last lesson
function getAuthenticatedUser(): UserPayload | null {
  const token = getCookie('auth_token');
  if (!token) {
    return null;
  }
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as UserPayload;
    return decoded;
  } catch (error) {
    return null;
  }
}

// The new server function that will be called by the client
export const getCurrentUserFn = server$(() => {
  return getAuthenticatedUser();
});

Step 2: Create the Authentication Context

Now, we create the context file. This is almost identical to the documentation example.

// src/contexts/auth.tsx
import { createContext, useContext, ReactNode } from 'react';
import { useServerFn } from '@tanstack/react-start';
import { getCurrentUserFn } from '../server/auth';

// Define the user type, matching the server payload
type User = {
  userId: string;
  email: string;
  role: string;
};

// Define the shape of our context
type AuthContextType = {
  user: User | null;
  isLoading: boolean;
  refetch: () => void; // Allows us to manually refetch the user
};

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  // `useServerFn` is the magic!
  // 1. During SSR, it runs `getCurrentUserFn` on the server and gets the data.
  // 2. The data is serialized and sent to the client with the HTML.
  // 3. On the client, it hydrates with this initial data.
  // 4. Subsequent calls (like `refetch`) are made via fetch requests.
  const { data: user, isLoading, refetch } = useServerFn(getCurrentUserFn);

  return (
    <AuthContext.Provider value={{ user, isLoading, refetch }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

Step 3: Wrap Your App with the Provider

To make the context available everywhere, we wrap the application's root component with AuthProvider.

// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router';
import { AuthProvider } from '~/contexts/auth'; // Import the provider

export const Route = createRootRoute({
  component: () => (
    <AuthProvider>
      {/* ... your existing layout (nav, etc.) ... */}
      <Outlet />
    </AuthProvider>
  ),
});

With this in place, any component in your app can now call useAuth() to get the user's status, a loading state, and a function to refetch their identity.


3. Approach 2: TanStack Router Context

While the React Context pattern is excellent, it's valuable to understand an alternative that's deeply integrated into the router itself. TanStack Router has its own "Context" feature, designed for dependency injection.

The main idea is to define a typed context when you create your root route and then inject the necessary authentication state when you initialize the router.

Router Context | TanStack Router React Docs

To grasp the fundamentals, let's review the official TanStack Router documentation on this feature.

First, read 'Typed Router Context' to see the createRootRouteWithContext function. Then, read 'Passing the initial Router Context' to understand how the context option is used when creating the router. Finally, review 'How about using React Context/Hooks?' to see the complete pattern of using a hook in your main App component and injecting its value into the RouterProvider.

This pattern is powerful because the context is available directly inside loader and beforeLoad functions, which can simplify dependency management for things like API clients or auth services.

TanStack Router: Authenticated Routes (Guards)

This video provides a very clear and practical demonstration of wiring up a router context for authentication.

Watch from 02:29 to 05:44. The video shows exactly how to: Define a RouterContext type. Use createRootRouteWithContext. Create a useAuth hook to manage state. Pass the auth state into the <RouterProvider />'s context prop.

Comparing the Approaches

Feature React Context (useServerFn) Router Context
Primary Use Sharing state with the React component tree. Dependency injection into router functions (loader, beforeLoad).
Setup Wrap app in <AuthProvider>. useServerFn handles data. Use createRootRouteWithContext, pass context to <RouterProvider>.
Data Access Via useAuth() hook in components. Via context property in loader/beforeLoad and useRouteContext() hook in components.
SSR Integration useServerFn is the built-in, first-class solution for SSR data fetching in TanStack Start. Requires more manual wiring to get server-side data into the initial context.

Recommendation: For our purposes, the React Context with useServerFn pattern is the preferred method. It is the most direct and well-supported solution for sharing authentication state between server and client in a TanStack Start application. Understanding the Router Context is beneficial for more advanced dependency injection scenarios you might encounter later.


4. Using the Authentication Context

Now that our AuthProvider is set up, using it is straightforward. Here's how you might create a component that displays different content based on the auth state.

// src/components/AuthStatus.tsx
import { useAuth } from '~/contexts/auth';
import { Link } from '@tanstack/react-router';

export function AuthStatus() {
  const { user, isLoading } = useAuth();

  if (isLoading) {
    // Show a placeholder or spinner during the initial load
    return <div className="h-8 w-24 animate-pulse rounded bg-gray-300" />;
  }

  if (user) {
    return (
      <div className="flex items-center gap-4">
        <span>Welcome, {user.email}!</span>
        <a href="/logout">Logout</a> {/* We will implement this later */}
      </div>
    );
  }

  return <Link to="/login">Login</Link>;
}

You can now place this <AuthStatus /> component in your main layout (e.g., in __root.tsx) and it will correctly display the user's state on both the initial server-rendered page and on subsequent client-side navigations.

Conclusion

In this lesson, we successfully bridged the gap between our server-side authentication logic and our client-side UI.

Key Takeaways:

  • A shared authentication context is essential for a reactive UI in an SSR application.
  • The recommended pattern in TanStack Start is to use React Context combined with the useServerFn hook.
  • useServerFn is the key mechanism that fetches data on the server during SSR and handles client-side fetching after hydration, all through a single, unified API.
  • By defining a typed context (AuthContextType) and a consumer hook (useAuth), we gain type-safe access to the user's identity throughout our entire application.
  • The Router Context is a powerful alternative from TanStack Router, primarily used for dependency injection into route functions like loader and beforeLoad.

Next Up:

We have one final piece to perfect this setup. While useServerFn handles the data fetching, there's a subtle but important step: ensuring the initial state from the server is correctly "hydrated" on the client to prevent UI mismatches (e.g., a momentary flash of the "Login" button before it switches to "Logout"). In the next lesson, we will serialize and inject the initial authentication state from the server into the HTML document for client-side hydration, making our user experience perfectly seamless.

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

Sign up