React hydration mismatches happen when the server-rendered HTML doesn’t match what the client-side React app expects, causing the client to discard the server’s work and re-render everything.

Here’s how to find and fix SSR drift:

Common Causes and Fixes

  1. Client-Side Only APIs/Logic:

    • Diagnosis: Check your client-side code for any use of browser-specific APIs (like window, document, localStorage) or logic that assumes a browser environment before React has mounted. This often happens in useEffect or directly in the component body.
    • Fix: Wrap client-only code in useEffect hooks. This ensures the code only runs on the client after the component has mounted and the DOM is available.
      import React, { useState, useEffect } from 'react';
      
      function MyComponent() {
        const [windowWidth, setWindowWidth] = useState(0);
      
        useEffect(() => {
          // This code only runs on the client
          setWindowWidth(window.innerWidth);
          const handleResize = () => setWindowWidth(window.innerWidth);
          window.addEventListener('resize', handleResize);
          return () => window.removeEventListener('resize', handleResize);
        }, []); // Empty dependency array ensures this runs only once on mount
      
        return <div>Window width: {windowWidth}</div>;
      }
      
    • Why it works: useEffect callbacks are guaranteed to run after the initial render on the client, preventing mismatches caused by client-specific operations during server rendering.
  2. Date/Time Differences:

    • Diagnosis: If your application displays dates or times, differences in server and client timezones or clock drift can cause mismatches.
    • Fix: Use a library like date-fns or moment.js to format dates consistently. Crucially, ensure you’re using UTC or a fixed timezone for server-rendered output and client-side formatting.
      import { format } from 'date-fns';
      import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
      
      // On the server, render a consistent UTC date
      const serverDate = new Date();
      const utcDateString = serverDate.toISOString(); // e.g., "2023-10-27T10:30:00.000Z"
      
      // On the client, format it, potentially to a specific timezone
      const clientDate = new Date(utcDateString);
      const formattedDate = format(clientDate, 'yyyy-MM-dd HH:mm:ss', { timeZone: 'America/New_York' });
      
    • Why it works: By rendering a standardized format (like ISO 8601 UTC) from the server and then parsing and formatting it on the client, you eliminate discrepancies caused by varying server/client clocks or default timezone interpretations.
  3. Random Number Generation:

    • Diagnosis: If you generate random numbers on the server and client independently, they will almost certainly differ.
    • Fix: Generate a seed on the server and pass it to the client, or use a deterministic random number generator. Alternatively, generate the random number only on the client after hydration.
      // Example: Generating random number on client after hydration
      import React, { useState, useEffect } from 'react';
      
      function RandomDisplay() {
        const [randomNumber, setRandomNumber] = useState(null);
      
        useEffect(() => {
          setRandomNumber(Math.random());
        }, []); // Runs only on client mount
      
        return <div>Random: {randomNumber !== null ? randomNumber.toFixed(4) : 'Loading...'}</div>;
      }
      
    • Why it works: Ensures the random value is consistent by generating it in an environment where it’s needed and controlled, or by making the generation process deterministic across server and client.
  4. Third-Party Libraries with DOM Dependencies:

    • Diagnosis: Some libraries might try to access the DOM or browser APIs during their initial setup, leading to mismatches if they run on the server.
    • Fix: Many libraries provide a noSsr or similar option. If not, dynamically import the component that uses the library within a useEffect hook.
      import React, { useState, useEffect } from 'react';
      
      function DynamicChart() {
        const [ChartComponent, setChartComponent] = useState(null);
      
        useEffect(() => {
          // Dynamically import the charting library component
          import('react-chartjs-2').then(module => {
            setChartComponent(() => module.Line); // Assuming Line is the component
          });
        }, []);
      
        if (!ChartComponent) {
          return <div>Loading chart...</div>;
        }
      
        // Render the chart only when the component is loaded
      
        return <ChartComponent data={{ /* chart data */ }} />;
      
      }
      
    • Why it works: Delaying the import and rendering of the component until the client-side environment is ready prevents the library from executing its DOM-dependent code during server rendering.
  5. Conditional Rendering Based on Non-Serializable State:

    • Diagnosis: If you’re conditionally rendering UI elements based on state that isn’t properly serialized from the server to the client (e.g., complex objects, functions), hydration can fail.
    • Fix: Ensure all initial state passed from the server to the client is serializable (JSON-compatible). Use JSON.stringify on the server and JSON.parse on the client, or use a library like next-serialize-javascript.
      // Server-side (e.g., in getServerSideProps in Next.js)
      const initialState = { count: 0, user: { name: 'Alice' } };
      return { props: { initialState: JSON.stringify(initialState) } };
      
      // Client-side component
      function MyComponent({ initialState }) {
        const [state, setState] = useState(JSON.parse(initialState));
        // ... rest of component
      }
      
    • Why it works: Guarantees that the exact same data structure and values are available to React on both the server and client for initial state comparison.
  6. CSS Classname Mismatches:

    • Diagnosis: Sometimes, CSS-in-JS libraries or manual classname generation can produce different class names on the server and client due to different rendering orders or internal state.
    • Fix: Configure your CSS-in-JS library for SSR or ensure deterministic classname generation. For example, with Emotion, you’d typically use cache.extractCritical() on the server and hydrate on the client.
      // Example with Emotion (simplified)
      // Server:
      import createEmotionServer from 'create-emotion-server';
      import createCache from '@emotion/cache';
      
      const cache = createCache({ key: 'css' });
      const { extractCritical } = createEmotionServer(cache);
      
      // ... render app, get html and css
      const { html, css, ids } = extractCritical(appHtml);
      // Send html and css to client
      
      // Client:
      import createCache from '@emotion/cache';
      import { hydrate } from 'emotion';
      
      const cache = createCache({ key: 'css' });
      hydrate(cache, document.querySelectorAll('style[data-emotion]'));
      
    • Why it works: Ensures that the CSS rules and their corresponding class names applied on the server are correctly recognized and applied by the client’s styling engine.

The next error you’ll encounter after fixing hydration mismatches is often related to client-side routing not matching the server-rendered initial page, leading to a full page reload instead of a seamless SPA transition.

Want structured learning?

Take the full React course →