Create your own
Lesson illustration

Client-Side Auth Hydration & UI Consistency

Hello! Let's dive into our next lesson.

Introduction

In our last session, we explored the "Dehydrate-Inject-Hydrate" pattern, understanding how TanStack Start automatically serializes server-side data (like our user's authentication status) and injects it into the HTML document. This ensures the data is available to the client without a follow-up network request.

Today, we'll focus on the final piece of this puzzle, addressing the learning outcome: Initialize the client-side authentication context from the hydrated state, implementing logic to prevent UI mismatches.

We will cover why UI mismatches happen, what a "hydration error" is, and how to structure our client-side code to correctly consume the hydrated state. This will ensure that the UI rendered by the server is identical to the initial UI rendered on the client, providing a seamless and error-free user experience.


1. The Problem: Hydration Mismatches

When a browser receives a server-rendered HTML page, it displays it immediately. Then, React loads and executes on the client, intending to "take over" the existing HTML and make it interactive. This process is called hydration.

A hydration mismatch, or error, occurs when the HTML that React generates during its initial client-side render does not exactly match the HTML that the server sent. When this happens, React can't reliably take over the DOM, leading to a warning in development and potential bugs in production.

Common causes include:

  • Using browser-specific APIs like window or localStorage during the initial render.
  • Generating random numbers or timestamps that differ between server and client.
  • Rendering content based on state that is only available on the client (e.g., from a useEffect hook that hasn't run yet).

Next.js Hydration failed because the initial ui does not match what was rendered on the server (FIX)

To see concrete examples of what causes these errors, let's watch a few segments from the video "Next.js Hydration failed... (FIX)" by ByteGrad. Although it focuses on Next.js, the underlying principles are universal to all SSR frameworks.

Watch the following three parts: Incorrect HTML Formatting (00:10 - 01:57): Understand how structurally invalid HTML can cause mismatches. Third-Party Components and Client-Side Rendering (02:34 - 05:17): See how components relying on client-only APIs are a frequent source of errors. Date and Timezone Discrepancies (05:51 - 07:18): Note how environmental differences between server and client can lead to different outputs. Focus on why these scenarios create a discrepancy between the server's output and the client's initial render.

In our authentication scenario, a mismatch would occur if the server renders a "Logout" button (for an authenticated user) but the client, not yet aware of the user's status, initially renders a "Login" button. Our goal is to prevent this.


2. The Solution: Synchronizing Initial State

As we've established, the solution is to ensure our client-side AuthContext is initialized with the exact same data that was used on the server. Because we used useServerFn in our AuthProvider, TanStack Start handles the hard parts for us.

Let's review the end-to-end flow:

  1. On the Server:

    • A request comes in.
    • Our root component renders, including our AuthProvider.
    • AuthProvider calls useServerFn('getCurrentUser'). Since this is the server, the function executes directly, retrieves the user from the request (e.g., via a cookie), and returns the user data.
    • The query cache is populated with this user data.
    • React renders the HTML (e.g., showing a "Welcome, User!" message).
    • TanStack Start automatically dehydrates the query cache and injects the user data into a <script> tag in the final HTML.
  2. On the Client:

    • The browser parses the HTML and displays the server-rendered content.
    • The client-side JavaScript bundle is downloaded and executed.
    • Crucially, before React starts rendering, TanStack Start's client entry script finds the injected data and uses it to hydrate the client-side TanStack Query cache.
    • Now, React begins its initial render.
    • Our AuthProvider renders and calls useServerFn('getCurrentUser').
    • The hook checks the query cache, finds the pre-populated user data, and returns it synchronously. No network request is made.
    • React renders the client-side HTML (showing "Welcome, User!").

Because the client's useServerFn hook synchronously returned the same data the server had, the initial client render produces identical HTML to what the server sent. The mismatch is avoided.

This automatic dehydration and hydration of TanStack Query data is the cornerstone of state management in TanStack Start.


3. Integrating State with the Router Context

While the automatic hydration of useServerFn handles the data flow, TanStack Router provides a more explicit and powerful way to make global state, like authentication, available throughout your application: the Router Context.

By defining a context at the router level, we can ensure that our authentication status is accessible in router-aware functions like loader and beforeLoad—which is essential for creating protected routes, our next topic.

Let's see how to set this up.

TanStack Router: Authenticated Routes (Guards)

The video "TanStack Router: Authenticated Routes (Guards)" by Dev Leonardo provides a clear, step-by-step guide on integrating a custom context with the router. We'll use this as our blueprint.

Watch the segment from 02:41 to 05:53. Pay close attention to these key steps: Defining a RouterContext interface. Using createRootRouteWithContext to make the router aware of this context type. Passing the actual context object to the <RouterProvider />. Accessing the context within a route's beforeLoad function.

Let's break down how we would apply this pattern to our application.

Step 1: Define the Context Type in router.tsx

First, we augment the TanStack Router module to declare the shape of our context. This gives us type safety everywhere.

// src/router.tsx

import { createRouter as createTanstackRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
import { type AuthContext } from './AuthContext'; // Assuming our context type is exported

// ... (other imports)

export function createRouter() {
  return createTanstackRouter({
    routeTree,
    context: {
      auth: undefined!, // We'll provide this at runtime
    },
  });
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof createRouter>;
  }
  // This is where we define the shape of the context
  interface RouterContext {
    auth: AuthContext;
  }
}

Note: The undefined! is a common pattern to satisfy TypeScript during the initial definition, as the actual context will be supplied when the router is used.

Step 2: Provide the Context in app.tsx

Next, in our main app component, we'll use our useAuth hook and pass its value to the RouterProvider. This connects our React state to the router's context.

// src/app.tsx

import { RouterProvider } from '@tanstack/react-router';
import { useAuth } from './AuthContext';
import { createRouter } from './router';

// Create the router instance
const router = createRouter();

function App() {
  const auth = useAuth(); // Our auth hook from previous lessons

  return (
    // Pass the auth state into the router provider
    <RouterProvider router={router} context={{ auth }} />
  );
}

Now, the authentication state is available to the entire routing system. When our useAuth hook updates (e.g., after login or logout), the new context is passed to the router, and any route-level logic that depends on it will have access to the latest state.

The beauty of this is that it works seamlessly with the hydration process. On the initial client render, useAuth() will synchronously return the hydrated user data, which is then immediately passed into the router's context before any route loaders or checks are run.


4. An Alternative View: The Manual Hydrator Pattern

To solidify your understanding, it's helpful to see how you might solve this problem more manually. This pattern is common in other frameworks or situations where automatic hydration isn't available.

Cursed Server Context Patterns In Next.js 14 | by MrManafon

The article "Cursed Server Context Patterns In Next.js 14" describes a pattern called "Remotely Hydrated Client Context". It's a clever way to get server-fetched data into a client-side context.

Read the section "Remotely Hydrated Client Context". Don't worry about the Next.js-specific syntax. Focus on the core idea: The server calculates some data (alternateLinks in the example). It passes this data as a prop to a simple, null-rendering client component (AlternateUrlContextHydrator). That client component uses useEffect to take the prop and update a shared React context. This demonstrates the principle of passing data from the server via props to "hydrate" a client-side state container.

This manual pattern highlights the two distinct stages: data transfer (server component passing props to a client component) and state update (the client component updating the context). TanStack Start's useServerFn elegantly combines both steps into one automated process for you.

Conclusion

You have now completed the loop on client-side state initialization. You understand not just that it works, but how it works, and why it's so critical for a good user experience in an SSR application.

Key Takeaways:

  • Hydration Mismatches occur when the server-rendered HTML differs from React's initial client-side render, often caused by client-only APIs or logic.
  • TanStack Start prevents this for our authentication state by automatically dehydrating the useServerFn result on the server and hydrating the client-side query cache before the app renders.
  • This ensures our useAuth hook has the correct user data synchronously on the first client render, guaranteeing the UI matches the server's output.
  • The Router Context is the proper, type-safe mechanism for making global state like authentication available to the entire routing system, including loaders and route guards.

Next Up:

With a stable and synchronized authentication context on both the server and client, we are finally ready to protect parts of our application. In the next lesson, we will implement protected routes using a typed, reusable beforeLoad function for server-side authentication checks.

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

Sign up