状態管理 (State)
Webアプリが「アプリ」らしく動くために欠かせないのが 「状態 (State)」 です。
このページでは、React/Next.js における状態管理の基本 useState について解説します。
状態(State)とは?
コンポーネントが「覚えておくべき情報」のことです。
例えば、「入力フォームに入力中の文字」、「カウントアップの数字」、「モーダルが開いているかどうか」などが状態にあたります。
変数が「ただの入れ物」だとすれば、State は「 変更されると画面(UI)を自動的に更新する入れ物 」と言えます。
基本的な使い方 (useState)
Next.js (App Router) で State を使う場合は、必ず Client Component ("use client") にする必要があります。
Server Component は「サーバーで一度だけ描画される」ものなので、ユーザーの操作に応じて変化する State を持つことができません。
カウンターを作ってみよう
クリックすると数字が増えるボタンを作ってみましょう。
src/app/counter/page.tsx"use client"; // 必須! import { useState } from "react"; export default function CounterPage() { // [今の値, 値を更新する関数] = useState(初期値) const [count, setCount] = useState(0); return ( <main className="p-8"> <h1 className="text-2xl font-bold mb-4">カウンター</h1> <p className="text-xl mb-4">現在のカウント: {count}</p> <button onClick={() => setCount(count + 1)} className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" > 増やす </button> <button onClick={() => setCount(0)} className="ml-4 bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600" > リセット </button> </main> ); }
フォームの入力値を管理する(オブジェクトの状態)
複数の入力項目がある場合、個別に useState を作ると大変です。オブジェクトとしてまとめて管理することも可能です。
src/app/signup/page.tsx"use client"; import { useState } from "react"; export default function SignupPage() { // オブジェクトで状態を管理 const [user, setUser] = useState({ name: "", email: "", }); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { // スプレッド構文 (...) で既存の値をコピーしつつ、新しい値で上書き setUser({ ...user, [e.target.name]: e.target.value }); }; return ( <form className="p-8 space-y-4"> <div> <label>名前</label> <input name="name" value={user.name} onChange={handleChange} className="border p-2 block w-full" /> </div> <div> <label>メール</label> <input name="email" value={user.email} onChange={handleChange} className="border p-2 block w-full" /> </div> <p>入力内容: {user.name} / {user.email}</p> </form> ); }
複雑な状態管理 (useReducer)
状態の更新ロジックが複雑になってきた場合(例:前の状態に依存する、複数の値が連動して変わる)、useState よりも useReducer が適しています。
src/app/counter-reducer/page.tsx"use client"; import { useReducer } from "react"; // 状態の型 type State = { count: number }; // アクションの型(何をするか) type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' }; // Reducer関数: (今の状態, アクション) => 新しい状態 const reducer = (state: State, action: Action): State => { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; case 'reset': return { count: 0 }; default: return state; } }; export default function ReducerPage() { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <main className="p-8"> <h1>Count: {state.count}</h1> <div className="space-x-2"> {/* dispatchでアクションを送る */} <button onClick={() => dispatch({ type: 'decrement' })} className="border p-2">-</button> <button onClick={() => dispatch({ type: 'increment' })} className="border p-2">+</button> <button onClick={() => dispatch({ type: 'reset' })} className="border p-2">Reset</button> </div> </main> ); }
状態の受け渡し (Props)
親コンポーネントで管理している State を、子コンポーネントで使いたい場合は Props (プロップス) で渡します。
バケツリレー (Props Drilling)
sample.tsx// ParentComponent export default function Parent() { const [count, setCount] = useState(0); // 子に count と setCount を渡す return <Child count={count} onClick={() => setCount(count + 1)} />; } // ChildComponent function Child({ count, onClick }: { count: number, onClick: () => void }) { // さらに孫へ... return <GrandChild count={count} onClick={onClick} />; }
親から子、子から孫へと State を渡していくことを「バケツリレー」と呼びます。
階層が浅ければ問題ありませんが、深くなると管理が大変になります(Props Drilling 問題)。これを解決するのが、次章で解説する Context や Global State です。
まとめ(基本編)
useState: 基本的な状態管理。まずはこれを使う。useReducer: 複雑な更新ロジックが必要な場合に使う。- Props : 親から子へ State を渡す基本的な手段。
基本的には useState と Props だけで多くのことができます。しかし、アプリが大規模になるとそれだけでは管理しきれなくなります。
次章の「応用編」では、より高度な状態管理について解説します。
更新履歴
- 2025-01-04 : ページのレイアウト崩れ・誤字を修正しました。