【Next.js】layout.tsxが再評価されず古い情報が残り続けた話

JavaScript

こんにちは、かつコーチです。

前回はNext.jsでよく出る定番エラーをまとめましたが、今回はもう少し踏み込んだ、経験しないと気づきにくいつまずき事例を紹介します。

テーマは「layout.tsxで取得したログイン状態が、ログアウトした後も画面に残り続けてしまう」という問題です。

App Routerの設計を正しく理解していないと、本番環境で気づきにくいバグを作り込んでしまう典型例なので、ぜひ押さえておいてください。

前提知識・何が起きるか

App Routerのlayout.tsxは、同じレイアウトを共有する複数のページ間を移動している間、基本的に再レンダリングされません。

これはNext.jsのパフォーマンス上の設計で、ナビゲーションのたびにヘッダーやサイドバーを毎回作り直さないための仕組みです。

一見便利な仕組みなのですが、これが思わぬ落とし穴になります。

layoutの中でユーザーのログイン状態をサーバー側で取得し、その結果をヘッダー表示などに使い回していると、ページ遷移だけではlayoutが再評価されないため、ログアウトした後もログイン中の表示が残り続けてしまうことがあります。

// app/layout.tsx
import { getCurrentUser } from "@/lib/auth";
import { Header } from "@/components/Header";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getCurrentUser(); // ここが再評価されない

  return (
    <html lang="ja">
      <body>
        <Header user={user} />
        {children}
      </body>
    </html>
  );
}

getCurrentUser()はCookieを見てログイン状態を判定する関数なのですが、ページ遷移だけではこの関数が再実行されず、古いユーザー情報を持ったヘッダーが表示され続けてしまいます。

基本の書き方・実装手順

対処法1: ログイン状態の取得をpage側かClient Componentに移す

一番シンプルな対処法は、ログイン状態の取得をlayoutから外し、各pageやClient Componentに任せることです。

// ❌ Before: layoutでユーザー情報を取得して使い回す
// app/layout.tsx
export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const user = await getCurrentUser();
  return (
    <html lang="ja">
      <body>
        <Header user={user} />
        {children}
      </body>
    </html>
  );
}
// ✅ After: HeaderをClient Componentにして自分でユーザー情報を取りに行く
// app/layout.tsx
import { Header } from "@/components/Header";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <Header />
        {children}
      </body>
    </html>
  );
}
// components/Header.tsx
"use client";

import { useEffect, useState } from "react";
import type { User } from "@/types/user";

export function Header() {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    fetch("/api/me")
      .then((res) => (res.ok ? res.json() : null))
      .then(setUser);
  }, []);

  return (
    <header>
      {user ? <p>ようこそ、{user.name}さん</p> : <p>ログインしていません</p>}
    </header>
  );
}

Client Component側で状態管理をすれば、ログイン・ログアウトのたびにuseEffectや状態更新のロジックで最新の情報を取りに行けるため、layoutの再評価タイミングに依存しなくなります。

対処法2: Cookieの変化を検知する仕組みを使う

もう一つの方法は、Cookieの変化を検知してUIを更新する仕組みを組み込むことです。

たとえばログアウト処理の中でCookieを削除した直後に、明示的に画面をリフレッシュするアプローチがあります。

// components/LogoutButton.tsx
"use client";

import { useRouter } from "next/navigation";

export function LogoutButton() {
  const router = useRouter();

  const handleLogout = async () => {
    await fetch("/api/logout", { method: "POST" });
    router.refresh(); // サーバー側の再取得を明示的に走らせる
  };

  return <button onClick={handleLogout}>ログアウト</button>;
}

router.refresh()を呼ぶと、現在のルートに対応するServer Componentがサーバー側で再取得されます。

ただしlayout自体がキャッシュされている場合は効果が薄いこともあるため、根本的にはログイン状態の判定ロジックをlayoutから切り離しておくほうが安全です。

つまずきポイント・一次情報の体験談

私が実際にこの問題にハマった経験を共有します。

社内向けの管理画面を作っていたとき、ヘッダーにログインユーザー名を表示する処理をlayout.tsxに書いていました。

動作確認では、ページをリロードして確認していたので何の問題もありませんでした。

ところが、QAチームから「ログアウトしたのに別のユーザーの名前が表示されたままになる」という報告を受けました。

再現手順を聞くと、リロードは一切せずにサイドバーのリンクをクリックしてページ遷移だけを繰り返していたとのことでした。

再現手順:
1. ユーザーAでログイン
2. サイドバーの「設定」をクリック(リンク遷移、リロードなし)
3. ログアウトボタンをクリック
4. サイドバーの「ダッシュボード」をクリック
5. ヘッダーにまだユーザーAの名前が表示されている

原因を調べているときは「Cookieの削除に失敗しているのでは」とサーバー側のログを疑っていたのですが、実際にはCookieは正しく削除されており、単純にlayoutが再評価されていなかっただけでした。

console.logをlayout.tsxの中に仕込んで、ページ遷移時に本当に実行されているかを確認したところ、初回アクセス時にしか呼ばれていないことが分かり、ようやく原因にたどり着けました。

App Routerのキャッシュ・再評価の仕組みは公式ドキュメントを読んだつもりになっていても、実際に手を動かして再現させるまで実感が湧きにくい部分だと感じています。

まとめ

この記事のポイント

  • layout.tsxはページ遷移のたびに再評価されるとは限らず、同じレイアウトを共有する間は使い回されることがある
  • layoutでログイン状態などの可変情報を取得して使い回すと、ログアウト後も古い情報が残るバグにつながる
  • 対処法としては、状態管理をpage側やClient Componentに移す方法が確実
  • router.refresh()でサーバー側の再取得を促す方法もあるが、根本対処にはならないケースもある
  • 「リロードすれば直る」バグはユーザーの実際の操作パターンでは再現しやすいので、遷移のみでの動作確認も忘れない

次に読むべき記事

次回は、Pages Router完全削除後に古いサードパーティライブラリが原因で発生する「Module not found: next/router」エラーについて解説します。

タイトルとURLをコピーしました