TL;DR
Swap useLayoutEffect for useEffect โ that alone fixes ~80% of cases. If you genuinely need layout-timing on the client, use an isomorphic hook that falls back to useEffect on the server.
// Quick fix โ works for most cases
import { useEffect } from 'react';
// Replace this:
// useLayoutEffect(() => { ... }, [deps]);
// With this:
useEffect(() => { ... }, [deps]);
Why this warning fires
useLayoutEffect runs synchronously after the DOM is painted. No DOM on the server means React can't execute or serialize its callback โ so it skips it and shouts this warning at you.
The real danger isn't the warning itself. It's the hydration mismatch that follows: the server HTML and the client-rendered HTML diverge, causing a visible flash or broken layout on first load.
You'll see it in three common situations:
- Next.js App Router or Pages Router โ any Client Component that calls
useLayoutEffectdirectly - Custom Express/Fastify SSR using
renderToStringorrenderToPipeableStream - Third-party libraries that call
useLayoutEffectinternally โ Radix UI, some animation libraries, older styled-components versions
Fix 1 โ Just use useEffect (covers ~80% of cases)
Honest question: do you actually need to read or write the DOM before the browser paints? Setting state, subscribing to a store, firing analytics โ none of that needs layout timing. useEffect handles all of it with zero SSR warnings.
// Before
import { useLayoutEffect, useState } from 'react';
function MyComponent() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
setWidth(window.innerWidth); // doesn't need layout timing
}, []);
return Width: {width}
;
}
// After โ warning gone
import { useEffect, useState } from 'react';
function MyComponent() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return Width: {width}
;
}
Fix 2 โ Isomorphic useLayoutEffect hook
Some cases genuinely need layout timing: measuring a DOM node's dimensions, syncing scroll position before paint, positioning a tooltip relative to its anchor. For those, use an isomorphic wrapper โ useLayoutEffect on the client, useEffect on the server.
// hooks/useIsomorphicLayoutEffect.ts
import { useEffect, useLayoutEffect } from 'react';
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
// Usage โ no warning, correct behavior on both ends
import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
function Tooltip({ anchorRef }: { anchorRef: React.RefObject }) {
useIsomorphicLayoutEffect(() => {
if (!anchorRef.current) return;
const rect = anchorRef.current.getBoundingClientRect();
// position tooltip relative to anchor
}, [anchorRef]);
return ...;
}
This exact pattern ships inside react-redux, react-spring, and most other SSR-compatible UI libraries. It's battle-tested โ just copy it.
Fix 3 โ Mark the component client-only (Next.js App Router)
Some components have no useful server output: a floating action button, a toast container, a canvas animation. Don't bother SSR-ing them at all.
In App Router, 'use client' at the top of the file is enough โ useLayoutEffect is fine inside a client-only component:
// app/components/ToastContainer.tsx
'use client';
import { useLayoutEffect } from 'react';
// Safe here โ this file never runs on the server
In Pages Router, reach for dynamic with ssr: false:
// pages/index.tsx
import dynamic from 'next/dynamic';
const HeavyClientComponent = dynamic(
() => import('../components/HeavyClientComponent'),
{ ssr: false }
);
Fix 4 โ Suppress warnings from third-party libraries
Sometimes the culprit is a dependency you can't patch โ you only control it through props. A targeted suppression buys time while you wait for an upstream fix. Keep it surgical and document exactly why it's there:
// Suppress only for known library issue โ document why!
const originalError = console.error;
console.error = (...args: unknown[]) => {
if (
typeof args[0] === 'string' &&
args[0].includes('useLayoutEffect does nothing on the server')
) {
return; // remove once library X ships v2.1
}
originalError(...args);
};
Drop this in _app.tsx or your test setup file. Pull it out the moment the library ships a patch โ suppressed warnings are silent tech debt that bites you later.
Verifying the fix
- Run
next build && next start(or your SSR server in production mode). The warning only surfaces in SSR paths, not innext devclient-only renders โ so always test the production build. - Watch the terminal. No more
Warning: useLayoutEffect does nothing on the serverlines. - Open the page with JavaScript disabled (DevTools โ Settings โ Disable JavaScript). The server HTML should look correct โ no blank sections, no broken layout.
- Re-enable JS. Check the browser console for hydration mismatch warnings. A clean console means you're done.
Tip: hydration mismatches after switching to useEffect
Switched to useEffect and now seeing a hydration mismatch? The root cause is almost always a value that differs between server and client โ window.innerWidth, Date.now(), navigator.userAgent.
The fix: initialize state with a safe server-compatible value (null or 0), then update it in useEffect after hydration completes.
function ResponsiveComponent() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null; // server and first client render match exactly
return ...;
}

