TL;DR
useLayoutEffect を useEffect に置き換えるだけで、約80%のケースが解決します。クライアント側でレイアウトタイミングが本当に必要な場合は、サーバー側では useEffect にフォールバックするアイソモーフィックフックを使用してください。
// クイックフィックス — ほとんどのケースで有効
import { useEffect } from 'react';
// これを置き換える:
// useLayoutEffect(() => { ... }, [deps]);
// こちらに変更:
useEffect(() => { ... }, [deps]);
この警告が発生する理由
useLayoutEffect は DOMが描画された後に同期的に実行されます。サーバー上にはDOMが存在しないため、Reactはコールバックを実行またはシリアライズできず、この警告を出力してスキップします。
本当の問題は警告そのものではありません。その後に発生するハイドレーションの不一致です。サーバーのHTMLとクライアントでレンダリングされたHTMLが乖離し、初回読み込み時に画面のちらつきやレイアウトの崩れが生じます。
主に以下の3つの状況で発生します:
- Next.js App RouterまたはPages Router —
useLayoutEffectを直接呼び出すクライアントコンポーネント renderToStringまたはrenderToPipeableStreamを使用したカスタムExpress/Fastify SSR- 内部で
useLayoutEffectを呼び出しているサードパーティライブラリ — Radix UI、一部のアニメーションライブラリ、旧バージョンのstyled-componentsなど
修正1 — useEffectを使う(約80%のケースに対応)
正直に考えてみてください。ブラウザが描画する前にDOMを読み取ったり書き込んだりする必要が本当にありますか?状態の設定、ストアへのサブスクライブ、アナリティクスの送信 — これらはいずれもレイアウトタイミングを必要としません。useEffect であれば、SSRの警告なしにすべて処理できます。
// 変更前
import { useLayoutEffect, useState } from 'react';
function MyComponent() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
setWidth(window.innerWidth); // レイアウトタイミングは不要
}, []);
return Width: {width}
;
}
// 変更後 — 警告が解消される
import { useEffect, useState } from 'react';
function MyComponent() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return Width: {width}
;
}
修正2 — アイソモーフィックuseLayoutEffectフック
レイアウトタイミングが本当に必要なケースも存在します。DOMノードの寸法の計測、描画前のスクロール位置の同期、アンカーに対するツールチップの配置などです。そのような場合は、アイソモーフィックなラッパーを使用してください — クライアントでは useLayoutEffect、サーバーでは useEffect にフォールバックします。
// hooks/useIsomorphicLayoutEffect.ts
import { useEffect, useLayoutEffect } from 'react';
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
// 使用例 — 警告なし、クライアント・サーバー両方で正しく動作
import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
function Tooltip({ anchorRef }: { anchorRef: React.RefObject }) {
useIsomorphicLayoutEffect(() => {
if (!anchorRef.current) return;
const rect = anchorRef.current.getBoundingClientRect();
// アンカーに対してツールチップを配置
}, [anchorRef]);
return ...;
}
このパターンはreact-redux、react-spring、その他多くのSSR対応UIライブラリで採用されています。実績のあるパターンですので、そのままコピーして使用してください。
修正3 — コンポーネントをクライアント専用にする(Next.js App Router)
サーバー出力が不要なコンポーネントも存在します。フローティングアクションボタン、トーストコンテナ、キャンバスアニメーションなどです。そのようなコンポーネントはSSRを行う必要はありません。
App Routerでは、ファイルの先頭に 'use client' を追加するだけで十分です — クライアント専用コンポーネント内では useLayoutEffect を安全に使用できます:
// app/components/ToastContainer.tsx
'use client';
import { useLayoutEffect } from 'react';
// ここは安全 — このファイルはサーバーで実行されない
Pages Routerでは、ssr: false を指定した dynamic を使用してください:
// pages/index.tsx
import dynamic from 'next/dynamic';
const HeavyClientComponent = dynamic(
() => import('../components/HeavyClientComponent'),
{ ssr: false }
);
修正4 — サードパーティライブラリの警告を抑制する
問題の原因がパッチを当てられない依存ライブラリにある場合もあります — プロップスを通じてしか制御できないケースです。上流の修正を待つ間、ピンポイントな抑制で対処できます。範囲を最小限にとどめ、その理由を明確にドキュメント化してください:
// 既知のライブラリの問題に対してのみ抑制 — 理由を必ずドキュメント化!
const originalError = console.error;
console.error = (...args: unknown[]) => {
if (
typeof args[0] === 'string' &&
args[0].includes('useLayoutEffect does nothing on the server')
) {
return; // ライブラリXがv2.1をリリースしたら削除すること
}
originalError(...args);
};
このコードを _app.tsx またはテストのセットアップファイルに追加してください。ライブラリがパッチをリリースしたらすぐに削除しましょう — 抑制された警告は後々問題になるサイレントな技術的負債です。
修正の確認
next build && next start(またはSSRサーバーをプロダクションモードで)を実行してください。この警告はSSRパスでのみ発生し、next devのクライアント専用レンダリングでは表示されません。必ずプロダクションビルドでテストしてください。- ターミナルを確認します。
Warning: useLayoutEffect does nothing on the serverが表示されなければOKです。 - JavaScriptを無効にしてページを開いてください(DevTools → 設定 → JavaScriptを無効にする)。サーバーHTMLが正しく表示されることを確認します — 空白のセクションやレイアウトの崩れがないことをチェックしてください。
- JSを再度有効にして、ブラウザコンソールにハイドレーションの不一致の警告が表示されないことを確認します。コンソールがクリーンであれば完了です。
ヒント:useEffectへ切り替えた後のハイドレーション不一致
useEffect に切り替えた後にハイドレーションの不一致が発生していますか?根本的な原因はほぼ常に、サーバーとクライアントで異なる値にあります — window.innerWidth、Date.now()、navigator.userAgent などです。
解決策:サーバーと互換性のある安全な初期値(null または 0)で状態を初期化し、ハイドレーション完了後に useEffect 内で更新してください。
function ResponsiveComponent() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null; // サーバーと最初のクライアントレンダリングが完全に一致する
return ...;
}

