useReducer とは
useReducer
はuseState
と同様に state を管理することができるフックです。
useState
との使い分けは、state が複数の値にまたがるような複雑なロジックがある場合・前回の state に基づいて state を更新する場合にuseReducer
を使うのがよいとされています。
また、複数階層にまたがり更新が必要なコンポーネントにおいて、useReducer
を用いて dispatch を下位コンポーネントに渡すとパフォーマンスの最適化につながります。
それでは順を追って説明していきます。
useReducer の呼び出し方
まずuseReducer
の呼び出し方からです。以下のように呼び出します。
const [state, dispatch] = useReducer(reducer, initialState);
useReducer は以下の引数を受け取ります。
reducer
: state を更新するための関数。state と action を受け取って新しい state を返す。initialState
: state の初期値
戻り値として以下の配列を返します。
state
: state(コンポーネントの状態)dispatch
: reducer を実行するための呼び出し関数
まだあまりイメージを掴めないと思うので、例を見てみましょう。
以下は Todo リストのコンポーネントの例です(TypeScript を利用しています)。
import React, { useReducer, useState } from "react";
type State = {
id: number,
text: string,
};
type Action = {
type: "ADD",
text: string,
};
const initialState: State[] = [
{
id: 0,
text: "initial todo",
},
];
// stateとactionを受け取り、actionのtypeによってstateの更新方法を変える
const reducer = (state: State[], action: Action): State[] => {
switch (action.type) {
case "ADD":
return [...state, { id: state.slice(-1)[0].id + 1, text: action.text }];
}
};
const TodoList: React.FC = () => {
const [text, setText] = useState("");
const [todoList, dispatch] = useReducer(reducer, initialState);
return (
<div className="todos">
<label>
todo :
<input
type="text"
value={text}
onChange={e => setText(e.target.value)}
/>
</label>
<button onClick={() => dispatch({ type: "ADD", text: text })}>
Add todo
</button>
<ul>
{todoList.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
};
まず、以下のコードで、先ほどの定義通りuseReducer
を宣言しています。
const [todoList, dispatch] = useReducer(reducer, initialState);
この時第一引数にreducer
関数、第二引数に初期値の state を受け取っています。reducer
関数は
const reducer = (state: State[], action: Action): State[] => {
switch (action.type) {
case "ADD":
return [...state, { id: state.slice(-1)[0].id + 1, text: action.text }];
}
};
こちらで定義されており、state とaction
と呼ばれる次に何を行うかを示すオブジェクトを受け取ります。なおこれらの型も別に用意しておきます。
そしてreducer
関数の内部で action の type プロパティによって処理を分岐し、新しい state を返していることがわかります。
このreducer
を実行するのにはdispatch
が必要です。
例では以下の部分で実行しています。
<button onClick={() => dispatch({ type: "ADD", text: text })}>
dispatch
を実行することでreducer
を呼び出せます。なおこの時action
を引数にします。
このようにreducer
を介すことで複雑な state の更新のロジックをまとめることができます。
前回の state に依存して state を更新するユースケース
state を更新する時、setState(beforeState => ...)
のような書き方をすることがあります。しかしそのような場合は useReducer を用いた方がいいかもしれません。
以下は、setState(beforeState => ...)
を使った場合の例です。
const Counter: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// 前のstateを元に更新
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return (
<>
<div>count : {count}</div>
</>
);
};
この場合、問題なくコードは動きます。setCount
内にコールバック関数を入れることで前回の state を読み取ることができ、無限ループを回避できます。
しかし state が少し複雑になり 2 つ以上の state の値を元に更新するだったら、上記のような更新方法は望ましくありません。そういった場合にuseReducer
が役に立ちます。
以下のように state に count に加え step というプロパティが増えた場合、そもそもロジックが複雑になり reducer に切り出した方が見通しがいいです。
import React, { useEffect, useReducer } from "react";
type State = {
count: number,
step: number,
};
type Action = {
type: "COUNT" | "STEP",
};
const initialState: State = {
count: 0,
step: 0,
};
const reducer = (state: State, action: Action): State => {
// action.typeによってstateの更新を切り替え
switch (action.type) {
case "COUNT":
return { count: state.count + 1, step: state.step };
case "STEP":
return { count: state.count, step: state.step + 1 };
}
};
const Counter: React.FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
// 毎秒dispatchしてstateを更新する
const id = setInterval(() => {
dispatch({ type: "COUNT" });
}, 1000);
return () => clearInterval(id);
// dispatch関数はconstant
}, [dispatch]);
return (
<>
<div>count : {state.count}</div>
<div>step : {state.step}</div>
</>
);
};
export default Counter;
さらに注目すべきは、useEffect
の依存配列にdispatch
関数を入れていることです。通常のコンポーネント内に定義した関数はレンダリングごとにべつの関数になってしまうため、それを入れると無限ループに陥ってしまいます。
しかし、dispatch
関数はメモ化されたコールバック関数であるため、レンダリングごとで不変となります。よってこの例は始めのレンダリング時しか呼び出されず、無限ループを回避できます。
useReducer を用いたパフォーマンス最適化
先ほど説明した通りuseReducer
を用いるとdispatch
関数はメモ化されるため、React.memo
と組み合わせるとパフォーマンスの最適化が期待できます。
よく利用されるケースは、useContext
等を使ってコンテキスト経由で下の階層のコンポーネントにdispatch
関数を渡し、子コンポーネント等でそれを使い state を更新する、といったケースです。
また子コンポーネントをReact.memo
でメモ化し、props やコンテキスト経由でsetState
の代わりにdispatch
を渡すと不要なレンダリングを避けることができます。
まとめ
useReducer
は action・state を受け取り新しい state を返すreducer
、初期 state を受け取る- 現在の state、
reducer
を呼び出すdispatch
関数を返す
- 現在の state、
useReducer
は state が複数の値にまたがるような複雑なロジックがある場合・前回の state に基づいて state を更新する場合に用いるのがよいdispatch
関数はメモ化されているのでパフォーマンス最適化に使える
useReducer はかなり使えるケースが多いと思うので、是非積極的に使ってみてください 🙌