複雑な状態管理に向けて
先生!前回のStateの記事で、useState
を使って状態を管理する方法は分かりました。でも、大きなアプリだともっと複雑な状態管理が必要になるって聞きました!!
いい質問だね、OZくん!そのとおり。useState
は便利だけど、状態が多くなるとコードが煩雑になることがあります!そこで登場するのが、【useContext】
と【useReducer】
です。
useContext
とuseReducer
?それってどんなものなんですか?
簡単に言うと、useContext
はデータをコンポーネント間で簡単に共有するための仕組みで、useReducer
は複雑な状態管理をシンプルにするためのものなんだ。それじゃあ、順番に説明していこう!
1. useContext
って何?
グローバルな状態を共有する
useContext
を使うと、親から子へpropsで直接データを渡す方法ではなく、コンポーネントのどこからでもデータを受け取れるようになるんです。まず、例を見てみましょう!
import { createContext, useContext } from "react";
const UserContext = createContext();
const ParentComponent = () => {
const user = { name: "OZ", age: 18 };
return (
<UserContext.Provider value={user}>
<ChildComponent />
</UserContext.Provider>
);
};
const ChildComponent = () => {
const user = useContext(UserContext);
return (
<p>
こんにちは!{user.name}くん!年齢は、{user.age}ですね!
</p>
);
};
export default ParentComponent;
コード解説
createContext
- グローバルにデータを管理するための「箱」を作成。ここでは
UserContext
を作っています。
- グローバルにデータを管理するための「箱」を作成。ここでは
UserContext.Provider
- データ(
value
プロパティ)を子コンポーネントに共有するための仕組み。
- データ(
useContext(UserContext)
- 子コンポーネントがデータを受け取るときに使う。
これなら、親から子に毎回props
を渡さなくてもいいんですね!
2. 状態が複雑な場合に便利なuseReducer
useReducer
は、複雑な状態を管理するときに便利なフックです。useState
と似てるけど、状態の更新ロジックをreducer
という関数にまとめて書けるのが特徴です。
カウンターアプリの例
import { useReducer } from "react";
const initialState = { count: 0 };
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return initialState;
default:
throw new Error("不明なアクションです");
}
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
debugger;
return (
<div>
<p>カウント:{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>増やす</button>
<button onClick={() => dispatch({ type: "decrement" })}>減らす</button>
<button onClick={() => dispatch({ type: "reset" })}>リセット</button>
</div>
);
};
export default Counter;
カウンターアプリのコード解説
全体の流れ
- このアプリはカウンターを操作するもので、ボタンを押すと数値が増減したり、リセットされたりします。
- 状態管理に
useReducer
を使い、状態(state
)を更新するための「アクション」(dispatch
)を呼び出します。
const initialState = { count: 0 };
これは状態の初期値ですcount: 0
として、最初はカウンターが0に設定されます
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error('Unhandled action type: ' + action.type);
}
}
- 引数の説明
state
:現在の状態(例:{ count: 0 }
)action
:どのように状態を更新するかを指定するオブジェクト(例:{ type: 'increment' }
)
- switch文の動き
action.type
がincrement
の場合、count
を1増やした新しい状態を返しますdecrement
の場合、count
を1減らしますreset
の場合、initialState
に戻します- 想定外の
action.type
が渡された場合はエラーを投げます(バグ防止)
const [state, dispatch] = useReducer(reducer, initialState);
useReducer
は2つの引数を受け取ります。
- 状態を更新するための
reducer
関数 - 状態の初期値(
initialState
)
戻り値として、次の2つが返されます。
state
:現在の状態(例:{ count: 0 }
)dispatch
:状態を更新するための関数
(例:dispatch({ type: 'increment' })
)
return (
<div>
<p>カウント: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>増やす</button>
<button onClick={() => dispatch({ type: 'decrement' })}>減らす</button>
<button onClick={() => dispatch({ type: 'reset' })}>リセット</button>
</div>
);
state.count
:現在のカウント値を表示。各ボタン:onClick
イベントでdispatch
を呼び出し、状態を更新します。
increment
ボタン:カウントを1増やすdecrement
ボタン:カウントを1減らすreset
ボタン:初期状態にリセットする
3. useContext
とuseReducer
を組み合わせる
useContext
とuseReducer
を組み合わせると、もっと便利になりますよ。例えば、大規模アプリで複数のコンポーネント間で状態を共有しながら管理する場合を考えてみましょう!!
Todoリストの例
import React, { createContext, useReducer, useContext } from 'react';
// 初期状態とreducer関数
const initialState = [];
function reducer(state, action) {
switch (action.type) {
case 'add':
return [...state, action.payload];
case 'remove':
return state.filter((_, index) => index !== action.payload);
default:
throw new Error('Unhandled action type: ' + action.type);
}
}
// Contextの作成
const TodoContext = createContext();
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
}
function TodoList() {
const { state, dispatch } = useContext(TodoContext);
return (
<div>
<ul>
{state.map((todo, index) => (
<li key={index}>
{todo} <button onClick={() => dispatch({ type: 'remove', payload: index })}>削除</button>
</li>
))}
</ul>
<button
onClick={() => dispatch({ type: 'add', payload: `新しいタスク ${state.length + 1}` })}
>
タスクを追加
</button>
</div>
);
}
function App() {
return (
<TodoProvider>
<TodoList />
</TodoProvider>
);
}
export default App;
Todoリストのコード解説
全体の流れ
- このアプリはタスク(Todo)を管理するもので、新しいタスクを追加したり、削除したりできます。
- 複数のコンポーネントで状態を共有するため、
useReducer
とuseContext
を組み合わせています。
const initialState = [];
タスクが最初は何もない状態を配列[]
で定義しています。
function reducer(state, action) {
switch (action.type) {
case 'add':
return [...state, action.payload];
case 'remove':
return state.filter((_, index) => index !== action.payload);
default:
throw new Error('Unhandled action type: ' + action.type);
}
}
動作の説明
add
アクションの場合:action.payload
(新しいタスク)を現在の状態(配列)に追加します。[...state, action.payload]
はスプレッド構文を使って新しい配列を作成します。
remove
アクションの場合:- 指定されたインデックス(
action.payload
)を除く配列を返します。 filter
メソッドで不要なタスクを除外します。
- 指定されたインデックス(
const TodoContext = createContext();
TodoContext
を作成し、アプリ全体で状態を共有できる仕組みを用意します。
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
}
useReducer
で管理している状態とdispatch
をコンテキストに渡します。
子コンポーネントでTodoContext
を使えば、これらの値を利用できます。
function TodoList() {
const { state, dispatch } = useContext(TodoContext);
return (
<div>
<ul>
{state.map((todo, index) => (
<li key={index}>
{todo} <button onClick={() => dispatch({ type: 'remove', payload: index })}>削除</button>
</li>
))}
</ul>
<button
onClick={() => dispatch({ type: 'add', payload: `新しいタスク ${state.length + 1}` })}
>
タスクを追加
</button>
</div>
);
}
useContext
の利用
TodoContext
からstate
とdispatch
を取得します。
タスクの表示
state.map
で現在のタスクをループし、それぞれをリストアイテム(<li>
)として表示します。- 各アイテムには削除ボタンがあり、クリックすると
remove
アクションが呼び出されます。
タスクの追加
- 追加ボタンを押すと、
dispatch
でadd
アクションを呼び出し、新しいタスクを配列に追加します。
なるほど!こうすれば、どのコンポーネントからでもTodoリストを操作できますね!
次回に向けて
先生、今日もたくさん学べました!次はReduxの話を教えてください!
もちろん!Reduxは、さらに状態管理を強力にしてくれるライブラリです。次回を楽しみにしていてくださいね!