Fix 'useLayoutEffect does nothing on the server' Warning in React SSR / Next.js

intermediateโš›๏ธ React2026-04-08| React 16+, Next.js 12โ€“14, Node.js (SSR environment), any server-side rendering setup

Error Message

Warning: useLayoutEffect does nothing on the server because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI.
#react#nextjs#ssr#useLayoutEffect#hooks#server-rendering

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 useLayoutEffect directly
  • Custom Express/Fastify SSR using renderToString or renderToPipeableStream
  • Third-party libraries that call useLayoutEffect internally โ€” 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 in next dev client-only renders โ€” so always test the production build.
  • Watch the terminal. No more Warning: useLayoutEffect does nothing on the server lines.
  • 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 ...;
}

Related Error Notes