'use client' is not CSR
It was when I first started with Next.js. I decided to build a Toast notification component for better user experience.
These global UI elements are typically rendered at the top of the browser's DOM (usually document.body or #portal-root), regardless of the React component tree structure. Therefore, I implemented the component using ReactDOM.createPortal.
However, as soon as I started the development server, I encountered an unexpected error. It was a Hydration Mismatch Error.
Error message summary: "The HTML markup rendered on the server does not match the markup generated on the client."To solve this problem, I first added the 'use client' directive at the top of the file, which explicitly marks a component as a client component. This component clearly depended on the browser's window and document.
However, surprisingly, the error did not disappear. I had explicitly instructed the code using createPortal to run on the client, so why was Next.js still talking about markup mismatches?
Cause
The 'use client' directive declares that "the final rendering and interactions of this component occur on the client". However, this does not prevent the initial loading method of the page, i.e., the server rendering (SSR) stage.
In Next.js, 'use client' components are also pre-rendered on the server for performance optimization. Through this initial server rendering, HTML markup is sent to the client.
The client receives this markup and displays it in the browser. After JavaScript loads, React begins the process of connecting JavaScript logic and event handlers to the HTML structure sent by the server, which is called hydration.
Hydration errors occur when this server markup does not match the final markup generated by the client component.
Rendering Method Comparison
Pure CSR
Initial HTML is empty, and the screen is constructed after a large JS bundle loads (slow TTI)Next.js SSR
Server generates markup and sends it. After JS loads, React hydrates the markupcreatePortal needs to access and manipulate DOM nodes outside the React tree using the document object, but the server environment (Node.js) does not have a document object.
When this code runs or is incompletely processed during server rendering, an inevitable mismatch occurs between the markup the server sends to the client and the markup the client's JS expects.
Solution
So how can we solve this? We use the useEffect hook to ensure this component only runs in the client environment (DOM).
useEffect runs after the component is mounted, i.e., only in the browser environment, so it does not execute during the server rendering stage.
'use client';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
export default function Toast() {
const [mounted, setMounted] = useState(false);
// Ensures execution only on the client
useEffect(() => {
setMounted(true);
}, []);
// Renders nothing before mounting
if (!mounted) return null;
// Renders via portal after mounting
return createPortal(<div className='toast'>Toast content</div>, document.body);
}This way, the server renders null, and the portal is only created after hydration is complete on the client, so no markup mismatch occurs.
Summary
In Next.js, all components are server components by default.
The 'use client' directive indicates that the component will be used on the client, but the initial HTML is still rendered through SSR. Afterward, interactive features like event handling are activated through hydration.
On the other hand, server components are provided to the client only in pure HTML form, reducing the JS bundle size.
Bonus: What about 'use server'?
The 'use server' directive is a different concept from server components, meaning you will use Server Actions. It's used when handling server-side tasks like form submissions or data modifications.