Skip to main content

Fixing "Hydration failed because the initial UI does not match" in Next.js

 Few errors in the React ecosystem induce as much frustration as the dreaded hydration mismatch:

Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching <div> in <p>.

This error signals a fundamental disagreement between the HTML Next.js generated on the server and the Virtual DOM React attempted to construct on the client. It forces React to discard the server-rendered markup and regenerate the UI from scratch, causing layout shifts and effectively negating the performance benefits of SSR (Server-Side Rendering).

The Root Cause: How Hydration Works

To understand the fix, you must understand the mechanism.

  1. Server Phase: Next.js renders your component tree into an HTML string. This snapshot implies a specific state at time $T=0$.
  2. Transport: The HTML is sent to the browser and painted immediately (First Contentful Paint).
  3. Client Phase (Hydration): React loads its JavaScript bundle. It traverses the existing DOM nodes and attempts to attach event listeners to them.

The Crash: During step 3, React expects the DOM structure to match its Virtual DOM exactly. If the browser tampered with the HTML (due to invalid syntax) or if the logic produced different results on the client (due to window or localStorage), React flags a mismatch.

Scenario 1: The Invalid HTML Trap

The most common cause of this error is technically valid JSX that generates invalid HTML. Browsers are forgiving; if they encounter syntactically invalid HTML, they auto-correct it. React's hydration logic, however, is strict.

The Problem

The HTML specification dictates that a <p> tag cannot contain block-level elements like <div><section>, or headers (<h1><h2>).

If you render this:

// ❌ BAD
<p>
  <div className="alert">Error occurred</div>
</p>

The browser parses the HTML and "fixes" it by closing the <p> tag immediately before the <div>:

<p></p>
<div class="alert">Error occurred</div>
<p></p>

When React hydrates, it looks for a child div inside the p. It finds an empty p instead. Mismatch.

The Solution

Use semantic HTML correctly. If you need a wrapper, use a <div><section>, or <span> depending on context.

// ✅ GOOD: Use a span for inline elements inside a paragraph
<p>
  <span className="alert">Error occurred</span>
</p>

// ✅ GOOD: Use a div wrapper if the child is block-level
<div className="wrapper">
  <div className="alert">Error occurred</div>
</div>

Scenario 2: Browser-Only APIs (windowlocalStorage)

The second most common cause is rendering data that exists only on the client.

The Problem

If you access windowlocalStorage, or document directly in your component body or specifically in the initialization of a useState hook, the server (Node.js runtime) and client will diverge.

// ❌ BAD: Direct access to localStorage causing mismatch
import { useState } from 'react';

export default function ThemeToggler() {
  // On Server: localStorage is undefined (crash) or mocked to null.
  // On Client: localStorage returns 'dark'.
  // Result: Hydration mismatch.
  const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light');

  return <div className={theme}>Current Theme: {theme}</div>;
}

The Solution: The useMounted Hook pattern

To fix this, we must ensure the initial render matches the server (usually a default state) and update the state only after the component has mounted on the client.

While you can write a useEffect in every component, the cleanest approach is a custom hook.

1. Create useIsClient.ts

import { useState, useEffect } from 'react';

export function useIsClient() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return isClient;
}

2. Implement Safe Rendering

// ✅ GOOD: Defer client-specific rendering
import { useState, useEffect } from 'react';
import { useIsClient } from './hooks/useIsClient';

export default function ThemeToggler() {
  const isClient = useIsClient();
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    // This only runs on the client, after the initial render pass
    const storedTheme = localStorage.getItem('theme');
    if (storedTheme) {
      setTheme(storedTheme);
    }
  }, []);

  // Option A: Render nothing until hydration is complete (prevents flash of wrong theme)
  if (!isClient) return null;

  // Option B: Render a loading state or default server-safe skeleton
  return (
    <div className={theme}>
      Current Theme: {theme}
    </div>
  );
}

Scenario 3: Timestamps and Randomness

Rendering new Date() or Math.random() directly in the JSX will always fail because the execution time on the server differs from the execution time on the client.

The Solution: suppressHydrationWarning

For simple text content mismatches (like timestamps) where the difference is acceptable and expected, React provides an escape hatch.

export default function Timestamp() {
  const time = new Date().toLocaleTimeString();

  return (
    <span suppressHydrationWarning>
      Last updated: {time}
    </span>
  );
}

Note: Only use suppressHydrationWarning for text content. Do not use it to mask structural HTML mismatches or logic errors, as it only suppresses the warning one level deep.

Summary

Resolving hydration errors is not just about silencing a console warning; it is about ensuring the integrity of your application's rendering pipeline.

  1. Validate your nesting: Never put a <div> inside a <p>.
  2. Enforce Determinism: The initial render returned by your component must be identical on Node.js and the Browser.
  3. Two-Pass Rendering: If you need browser data (Window/LocalStorage), render a default state first, then update state inside useEffect.

Popular posts from this blog

Restricting Jetpack Compose TextField to Numeric Input Only

Jetpack Compose has revolutionized Android development with its declarative approach, enabling developers to build modern, responsive UIs more efficiently. Among the many components provided by Compose, TextField is a critical building block for user input. However, ensuring that a TextField accepts only numeric input can pose challenges, especially when considering edge cases like empty fields, invalid characters, or localization nuances. In this blog post, we'll explore how to restrict a Jetpack Compose TextField to numeric input only, discussing both basic and advanced implementations. Why Restricting Input Matters Restricting user input to numeric values is a common requirement in apps dealing with forms, payment entries, age verifications, or any data where only numbers are valid. Properly validating input at the UI level enhances user experience, reduces backend validation overhead, and minimizes errors during data processing. Compose provides the flexibility to implement ...

jetpack compose - TextField remove underline

Compose TextField Remove Underline The TextField is the text input widget of android jetpack compose library. TextField is an equivalent widget of the android view system’s EditText widget. TextField is used to enter and modify text. The following jetpack compose tutorial will demonstrate to us how we can remove (actually hide) the underline from a TextField widget in an android application. We have to apply a simple trick to remove (hide) the underline from the TextField. The TextField constructor’s ‘colors’ argument allows us to set or change colors for TextField’s various components such as text color, cursor color, label color, error color, background color, focused and unfocused indicator color, etc. Jetpack developers can pass a TextFieldDefaults.textFieldColors() function with arguments value for the TextField ‘colors’ argument. There are many arguments for this ‘TextFieldDefaults.textFieldColors()’function such as textColor, disabledTextColor, backgroundColor, cursorC...

jetpack compose - Image clickable

Compose Image Clickable The Image widget allows android developers to display an image object to the app user interface using the jetpack compose library. Android app developers can show image objects to the Image widget from various sources such as painter resources, vector resources, bitmap, etc. Image is a very essential component of the jetpack compose library. Android app developers can change many properties of an Image widget by its modifiers such as size, shape, etc. We also can specify the Image object scaling algorithm, content description, etc. But how can we set a click event to an Image widget in a jetpack compose application? There is no built-in property/parameter/argument to set up an onClick event directly to the Image widget. This android application development tutorial will demonstrate to us how we can add a click event to the Image widget and make it clickable. Click event of a widget allow app users to execute a task such as showing a toast message by cli...