This warning appears during React hydration when the className generated on the server differs from the one generated on the client. Commonly caused by CSS-in-JS libraries like styled-components, Material-UI, or conditional rendering that differs between server and client.
This hydration warning occurs when React compares the server-rendered HTML with the client-side React tree and discovers that the `className` prop on a component or element doesn't match between the two environments. During server-side rendering (SSR), React generates HTML on the server and sends it to the browser. When the JavaScript bundle loads on the client, React performs "hydration" - it attaches event listeners and initializes the React component tree by comparing it to the existing server-rendered HTML. If the className values differ, React detects this mismatch and logs a warning. This is particularly common with CSS-in-JS libraries (styled-components, Emotion, JSS) that generate dynamic class names. If the server generates `css-abc123` but the client generates `css-xyz789`, React flags this inconsistency. The mismatch can cause visual flashing, incorrect styles, or accessibility issues.
Check the console warning carefully - it typically shows both the server and client className values:
Warning: Prop `className` did not match.
Server: "css-abc123 MuiButton-root"
Client: "css-xyz789 MuiButton-root"Use React DevTools to inspect the component tree and identify which specific component is triggering the warning. Look for components that use CSS-in-JS libraries or conditional styling.
The babel plugin ensures deterministic class name generation. Install it:
npm install --save-dev babel-plugin-styled-componentsAdd to your .babelrc or babel.config.js:
{
"plugins": [
[
"babel-plugin-styled-components",
{
"ssr": true,
"displayName": true,
"preprocess": false
}
]
]
}For Next.js, add to next.config.js:
module.exports = {
compiler: {
styledComponents: true,
},
}Restart your development server after configuration changes.
Create a shared Emotion cache for both server and client. Create lib/createEmotionCache.ts:
import createCache from '@emotion/cache';
export default function createEmotionCache() {
return createCache({ key: 'css', prepend: true });
}In your pages/_app.tsx (Next.js):
import { CacheProvider } from '@emotion/react';
import createEmotionCache from '../lib/createEmotionCache';
const clientSideEmotionCache = createEmotionCache();
function MyApp({ Component, emotionCache = clientSideEmotionCache, pageProps }) {
return (
<CacheProvider value={emotionCache}>
<Component {...pageProps} />
</CacheProvider>
);
}For server-side, in pages/_document.tsx:
import createEmotionServer from '@emotion/server/create-instance';
import createEmotionCache from '../lib/createEmotionCache';
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => <App emotionCache={cache} {...props} />,
});
const initialProps = await Document.getInitialProps(ctx);
const emotionStyles = extractCriticalToChunks(initialProps.html);
return { ...initialProps, emotionStyles };
}
}Never use browser APIs during initial render. Use state with useEffect instead:
❌ Wrong - causes hydration mismatch:
function ThemeToggle() {
const isDark = localStorage.getItem('theme') === 'dark';
return <div className={isDark ? 'dark' : 'light'}>...</div>;
}✅ Correct - consistent server/client render:
function ThemeToggle() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
setIsDark(localStorage.getItem('theme') === 'dark');
}, []);
return <div className={isDark ? 'dark' : 'light'}>...</div>;
}The component renders with the default state on both server and client, then updates after hydration in the browser.
Version mismatches can cause different hash generation. Check your package.json and lock file:
npm list styled-components
npm list @emotion/react
npm list @mui/materialDelete node_modules and lock file, then reinstall:
rm -rf node_modules package-lock.json
npm installIf using monorepos or micro-frontends, ensure all packages use the exact same versions:
{
"dependencies": {
"styled-components": "6.1.8",
"@emotion/react": "11.11.3"
}
}As a last resort for components that must differ (timestamps, random IDs), suppress the specific warning:
function Timestamp() {
const [time, setTime] = useState(new Date().toISOString());
useEffect(() => {
setTime(new Date().toISOString());
}, []);
return (
<div suppressHydrationWarning>
{time}
</div>
);
}Warning: Only use this when you understand why the mismatch occurs and cannot fix it. Overuse can hide real bugs.
Understanding CSS-in-JS Hash Generation
CSS-in-JS libraries generate class names by hashing the styles and component metadata. The hash should be deterministic (same inputs = same output), but several factors can break this:
- Component identifiers: Styled-components uses component display names in hashes. Without the babel plugin, minified production builds generate different names than development.
- Style insertion order: Emotion and JSS maintain a style sheet insertion order. If server and client insert styles in different orders, class names may differ.
- Dynamic values: Avoid using Math.random(), Date.now(), or crypto.randomUUID() in style definitions.
Next.js App Router Considerations
With Next.js 13+ App Router and React Server Components, be aware of the server/client boundary:
- Server Components render once on the server and don't hydrate
- Client Components (marked with "use client") undergo full hydration
- Only Client Components can use browser APIs or interactive hooks
- Move CSS-in-JS styled components to Client Components when possible
Production vs Development Differences
The warning may appear only in development or only in production:
- Development builds include more debugging information (component names, stack traces)
- Production builds are minified, changing component identifiers
- NODE_ENV differences can trigger conditional code paths
- Always test SSR pages in production mode before deploying: npm run build && npm run start
Material-UI v4 vs v5+
Material-UI v5 switched from JSS to Emotion, requiring different solutions:
- v4: Use ServerStyleSheets and remove jss-server-side element
- v5+: Use Emotion cache as shown in the solutions above
- Migration guides: https://mui.com/material-ui/migration/migration-v4/
Debugging with React DevTools Profiler
Enable "Highlight updates" in React DevTools to visualize which components re-render during hydration. className mismatches often cause unnecessary re-renders, visible as flashing highlights.
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite