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
-
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 inuseEffector directly in the component body. - Fix: Wrap client-only code in
useEffecthooks. 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:
useEffectcallbacks are guaranteed to run after the initial render on the client, preventing mismatches caused by client-specific operations during server rendering.
- Diagnosis: Check your client-side code for any use of browser-specific APIs (like
-
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-fnsormoment.jsto 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.
-
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.
-
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
noSsror similar option. If not, dynamically import the component that uses the library within auseEffecthook.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.
-
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.stringifyon the server andJSON.parseon the client, or use a library likenext-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.
-
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 andhydrateon 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.