要点
- useReducerとは
- useReducerを使用するメリット
- useReducerの使い方
useReducerとは
ReactのHooksでstateを管理する時に真っ先に思い出されるのは「useState」だと思います。
useReducerとはそのuseStateとの代替品です。
useReducerを使用するメリット
useStateより優れている場面は、
- 複数の値にまたがる複雑なstateロジックがある
- 前のstateに基づいて次のstateを決める必要がある
- 複数階層にまたがってstateの更新を発生させる必要がある
ような時です。
例えば、
「stateをイベントの発生状況によって値の更新の仕方を変えたいんだよな」
「イベントが発生した時に、前のstate(int)に値を足して更新したいんだよな」
「親コンポーネントのstateの値を子コンポーネントのイベントで変更したいんだよな」
などといった時に役立ちます。
useReducerの使い方
では実際の使用方法です。
まずuseReducerの初期化の方法はこのようにします。
const [state, dispatch] = useReducer(reducer, initialState);
このままでは機能しないので必要な定数や関数を定義していきます。
import React, { useReducer } from "react";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
これは画面上に Count: 1 などのようにボタンカウントを表示するソースコードです。
まず、useReducerは初期化するとリデューサーと呼ばれる関数と、stateの初期値を引数に、それぞれdispatch関数とstateに代入されます。
この場合、初期化された際のstateの値はinitialStateの値なので{count: 0}となります。
dispatchに関しては実際にイベントを発火させるどう動くのかみた方が理解が早いと思うので、
イベント発火時の動きを解説します。
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
こちらのボタンをクリックするとdispatch({type: 'decrement'})が実行されます。
するとreducerを呼び出します。
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
この時のreducerの引数の値は、stateは現在のstateの値、actionはdispatchの引数である{type: 'decrement'}です。
つまりstateは初期値と同じなので、{count: 0}で、actionは{type: 'decrement'}となります。
この時のreducerの挙動としては、switch文でaction.typeの値によって分岐をしているので、
function reducer(state, action) {
//〜略〜
case 'decrement':
return {count: state.count - 1};
//〜略〜
}
が実行されます。
ここでreturnしているのは更新したstateです。
return {count: state.count - 1};
となっているため、新しいstateの値は{count: -1}となります。
このようにuseReducerではdispatchという関数を用いて、イベントの使い分け(action.typeの値によってstateの更新方法を使い分け)することができます。
ちなみに、今の例ではreducerの中で決められた値の更新をしていましたが、(action.typeがincrementの場合は +1, decrementの場合は -1)
dispatchの引数の値を使用して更新することもできます。
import React, { useReducer } from "react";
const initialState = { count: 1 };
function reducer(state, action) {
switch (action.type) {
case "double":
return { count: state.count * action.double };
default:
throw new Error();
}
}
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "double", double: 2 })}>+</button>
</>
);
}
この場合、ボタンを押下すると、reducerの引数のactionはdispatch関数の引数である{type: 'double', double: 2}が渡されるので、
return {count: state.count * action.double};
は、{count:2}となります。2回押せば、2×2で4, 3回押せば4×2で8となります。
次にコンポーネント間での共有です。
コンポーネントが増えてくると、親コンポーネント・子コンポーネント間でstateのやりとりをしたくなる時があります。
値を参照するだけであればpropsとして渡してあげれば可能です。
しかし、親コンポーネントのstateを子コンポーネントから変更したい場合はそのままでは変更できません。
その場合は、dispatchを渡します。
//親コンポーネント
import React, { useReducer } from "react";
import CountChange from "./CountChange.js";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<CountChange dispatch={dispatch} />
</>
);
}
//子コンポーネント
export default function CountChange({ dispatch }) {
return (
<>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
ここでは親コンポーネントのdispatch関数を、子コンポーネントであるCountChangeにpropsとして渡しています。
<CountChange dispatch={dispatch} />
それを受け取った子コンポーネントで、ボタンを押下するとdispatch関数が実行されので、親コンポーネントのreducerが実行されてstateが更新されます。
まとめ
useStateもuseReducerもどちらもstateを管理するHooksですが適材適所で使い分けできそうです。
コンポーネント内でしか参照しないような場合や、複雑なロジックを必要としない場合は、useStateで十分ですが、アプリケーションが大きくなればなるほど、stateの管理は煩雑になっていきます。
コンポーネント間でのstateを受け渡しした際の更新なども簡単にできますし、ロジックを明確に分岐できるので非常に便利です。
ぜひ使ってみてください!