背景
この教材をやっていた時に、ログインしたユーザーの情報をuseContextで保持して各画面で表示する、という機能で詰まったのでアウトプットしておく
発生した問題
ヘッダーメニューのリンクをクリックすると、stateで保存していたはずのloginUserがnullになってしまっていた
ログイン後、HomeコンポーネントではloginUserにユーザーデータが入っていた
前提:stateの管理構造
まず「何が消えるのか」を理解するために、ログインユーザーの状態管理の仕組みを見る。
LoginUserProvider.tsx — stateの定義
useStateとContextを組み合わせて、ログインユーザーの情報をアプリ全体で管理している
// LoginUserProvider.tsx
import { createContext, useState } from 'react';
import type { User } from '../types/api/user';
export type LoginUserContextType = {
LoginUser: User | null
setLoginUser: (user: User) => void
}
export const LoginUserContext = createContext<LoginUserContextType | undefined>(undefined);
export const LoginUserProvider = (props: { children: React.ReactNode }) => {
const { children } = props
const [LoginUser, setLoginUser] = useState<User | null>(null);
return (
<LoginUserContext.Provider value={{ LoginUser, setLoginUser }}>
{children}
</LoginUserContext.Provider>
);
};
ログイン処理の中でsetLoginUser({ id: 1, name: "太郎", email: "taro@example.com" })のようにデータを格納している
Router.tsx — Providerがルートを囲む構造
// Router.tsx
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { LoginUserProvider } from "./providers/LoginUserProvider";
import { Home } from "./components/pages/Home";
import { UserManagement } from "./components/pages/UserManagement";
export const Router = () => {
return (
<BrowserRouter>
<LoginUserProvider>
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/home/user_management" element={<UserManagement />} />
</Routes>
</LoginUserProvider>
</BrowserRouter>
);
};
LoginUserProviderがRoutes全体を囲んでいるので、/homeから/home/user_managementに遷移してもProviderがアンマウントされなければstateは保持される(今回詰まったところ)
原因:Chakra UIの<Link>が<a href>になっていた
Header.tsxで画面遷移のリンクを書く際、Chakra UIのLinkコンポーネントを使っていた
// ❌ 修正前:Header.tsx
import { Link } from "@chakra-ui/react";
export const Header = () => {
return (
<nav>
<Link href="/home">ホーム</Link>
<Link href="/home/user_management">ユーザー一覧</Link>
</nav>
);
};
Chakra UIの<Link href="...">は、内部的にHTMLの<a href="...">タグをレンダリングする。これがstateが消える原因だった
なぜ<a href>だとstateが消えるのか
ブラウザの通常の画面遷移(<a href>)
ユーザーがリンクをクリック
↓
ブラウザが新しいURLにHTTPリクエストを送る
↓
サーバーからHTMLを取得する
↓
ページ全体を最初から読み込み直す(フルリロード)
↓
JavaScriptも最初から実行し直す
↓
Reactアプリが最初から起動する
↓
useState の初期値(null)がセットされる
↓
ログイン時にsetLoginUserで入れたデータは消えている
これは、ブラウザのアドレスバーにURLを打ち込んでEnterを押すのと同じ動作。ページ全体が破棄されて、一から作り直される
SPAの画面遷移(React Routerの<Link>)
ユーザーがリンクをクリック
↓
React Router が <a> タグのデフォルト動作を preventDefault() で止める
↓
ブラウザにHTTPリクエストを送らせない(サーバーにアクセスしない)
↓
JavaScriptでURLだけを書き換える(History API を使用)
↓
React Router が新しいURLに対応するコンポーネントだけを差し替える
↓
ページ全体はリロードされない
↓
LoginUserProvider は生きたまま(アンマウントされない)
↓
useState のデータはそのまま保持される
解決:React Routerの<Link>に変更
// ✅ 修正後:Header.tsx
import { Link } from "react-router-dom";
export const Header = () => {
return (
<nav>
<Link to="/home">ホーム</Link>
<Link to="/home/user_management">ユーザー一覧</Link>
</nav>
);
};
変更点は2つ
-
import { Link } from "@chakra-ui/react"→import { Link } from "react-router-dom" -
hrefprop →toprop
これで画面遷移してもstateが保持されるようになった。
デバッグ方法について
問題の切り分けで役立ったポイントも残しておく
フルリロードが起きている場合
遷移するたびにこのログが出る
[vite] connecting...
[vite] connected.
これはViteの開発サーバーとのWebSocket接続が新しく確立されたことを意味する。
つまり、ページが最初から読み込み直されている
SPA遷移の場合
本来、Reactの場合は上記のログは表示されない。コンポーネントの差し替えだけが行われるので、Reactのログだけが出る
まとめ
<a href> |
React Router <Link>
|
|
|---|---|---|
| ページリロード | する | しない |
| useState のデータ | 消える | 残る |
| サーバーへのリクエスト | 発生する | 発生しない |
| 内部の仕組み | ブラウザのデフォルト動作 | History API + preventDefault |
| 使うべき場面 | 外部サイトへのリンク | アプリ内の画面遷移 |
感想
- そもそもReactを理解できていないから詰まった、理解不足だった
- Viteのコンソールログ(
[vite] connecting...)でフルリロードが起きているかどうかを判断できるのは、今後のデバッグでも使えそう
参考