はじめに
恥ずかしながら、私はこれまで useState
フックのみで状態管理を行ってきました。
しかし、useReducer
フックなるものを知り、その便利さに驚きました。そこで、その利便性を共有するためにこの記事を書くことにしました。
useReducer
を使うと、複数の関連する状態をひとまとめにして管理できるようになります。これは、複数の useState
を使っていたときよりも整理されていて、扱いやすくなります。ぜひ使いこなせるようになりたいですよね。
この記事を読むことで、以下のことが理解できるようになります。
useReducer
の基本的な使い方useState
とuseReducer
の比較と使い分け方useState
からuseReducer
への段階的な移行の手順
以前の私と同じように状態管理の複雑さに悩む人の役に立てれば幸いです。
では、早速見ていきましょう!
1. useReducer
の基本的な使い方
まず、React から useReducer
をインポートします。
import { useReducer } from "react";
そして呼び出します。
他のフックと同様にコンポーネントのトップレベルもしくは独自のカスタムフックでのみ呼び出しが可能です。
基本的なシグネチャは以下のようになります。
const [ state, dispatch ] = useReducer(reducer, initialState);
それぞれの要素を説明していきます。
- reducer:状態とアクションを引数に取り、新しい状態を返す関数
- initialState:状態の初期値
- state:現在の状態を保持する変数
- dispatch:リデューサにアクションを与えてトリガーする関数
おそらく、ここで「?」が付くのは reducer と dispatch かと思いますので詳しく説明します。
リデューサ関数の例
以下は、カウンターを管理するシンプルなリデューサです。
// 状態の型を定義
type State = { count: number };
// アクションの型を定義
type Action = { type: "increment" } | { type: "decrement" }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unknown action type');
}
}
このリデューサ関数は、increment および decrementという2つのアクションタイプを扱います。もちろんこれらのアクションは自分で設定することができます。
useReducer
を使用する際、リデューサ関数は一般的に switch 文で記述されます。switch 文の各 case において、次の状態(state)を計算して返します。
dispatch の例
dispatch 関数は、アクションをリデューサへ送るために使用されます。具体的には、ユーザーの操作やイベントハンドラ内で dispatch を呼び出し、状態を更新します。
例を見てみましょう。
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
上記のコンポーネントでは、ボタンのクリックイベントに応じて dispatch 関数を呼び出しています。
dispatch 関数の引数は、ユーザによって実行されたアクションです。任意の型の値を指定できます。慣例として、アクションは通常オブジェクトであり、type プロパティで識別され、他のプロパティでオプションの追加情報を保持します。
2. useState
と useReducer
の比較と使い分け方
それぞれの特徴を表にしてまとめてみました。
useState |
useReducer |
|
---|---|---|
コード | 短い。初期コードは少ない | 長い。リデューサ関数と dispatch が必要 |
可読性 | シンプルな state 更新に最適 | 複雑な state 更新に最適。ロジックの分離が可能 |
デバッグ | バグの原因特定が難しい場合がある | コンソールログで更新の原因(state, action)を追跡可能 |
テスト | コンポーネント全体でテスト | 純関数として単体テストが可能 |
好み |
useReducer で代用可能 |
useState で代用可能 |
React 公式は、「バグが頻繁に発生しておりコンポーネントのコードに構造を導入したい場合に、リデューサを利用することをお勧めします」とのことです。
しかし機能は同等なので、個人の好みやチームの規則に従って選択するのが良さそうです。
同じコンポーネントで useState
と useReducer
を両方使うことも可能です。
3. useState
から useReducer
への段階的な移行の手順
useState
から useReducer
への移行は、次の 3 つのステップで行うことができます。
1. イベントハンドラからアクションをディスパッチする
リデューサを使った状態管理は state を直接セットするのとは少し異なります。
React に対して state をセットして「何をするか」を指示するのではなく、イベントハンドラから「アクション」をディスパッチすることで「ユーザが何をしたか」を指定します。
例えば、以下のような useState
を使ってタスクを直接的に追加/変更/削除するイベントハンドラがあったとしたら、
const [tasks, setTasks] = useState(initialTasks);
// 新しいタスクを追加する
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
// 既存のタスクを変更する
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
// タスクを削除する
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}
このように変更します。
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}
イベントハンドラ内で state を直接更新するのではなく、dispatch にアクションタイプと state の更新に必要な情報を与えてリデューサを呼び出してあげるというイメージです。
2. state とアクションから次の state を返すリデューサ関数を書く
useReducer
においては、リデューサ関数が state のロジックを記述する場所です。
現在の state とアクションオブジェクトの 2 つを引数に取り、次の state を返します。
先ほどの tasks の変更のロジックは以下のようになります。
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
リデューサ関数は state (tasks) を引数として取るため、コンポーネントの外部で宣言することができます。これにより、インデントレベルが減り、コードが読みやすくなります。
3. useState
を useReducer
に置き換える
最後に、リデューサをコンポーネントに接続する必要があります。React から useReducer
フックをインポートし、呼び出してください。
以下の useState
を使った呼び出しを、
const [tasks, setTasks] = useState(initialTasks);
このように置き換えます。
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
フックの第一引数に先ほど作成したリデューサをセットしてやれば完成です!
必要であればリデューサを別ファイルに移動することもできます。
useReducer
を使い、関心を分離することで、コンポーネントのロジックが読みやすくなります。イベントハンドラはアクションをディスパッチすることで「何が起こったか」を指定し、リデューサ関数がそれらに対する「state の更新方法」を決定します。
おわりに
いかがだったでしょうか。
個人的には useReducer
は、useState
を多用した時のコンポーネント内のコードやロジックの煩雑さを解消してくれるという点で、すごく便利だなと感じました。
それにしても React hooks はとても奥が深いですね。またキャッチアップがあれば記事にします。
ここまでお読みいただきありがとうございました!
参考
- React 公式リファレンス
- mosya React(ハンズオンもあってオススメ👇)