はじめに
お疲れ様です!Kei_dev_1213と申します!
今回の記事では、最近個人開発でよく使うuseReducerについて解説をしたいと思います!
useReducerとは
一言でいうとReactにおける状態管理のためのフックです。
Reactにおける状態管理といえばuseStateですが、こちらとは何が異なるのでしょうか。
結論から言うと、useStateとできること自体は変わりません。
ただ、useReducerは複雑な状態ロジックを管理する場合
に適したものとなります。
まずは振り返りがてらuseStateの使い方を見てみましょう↓。
useStateの基本構文
const [state, setState] = useState(initialValue);
-
state
: 現在の状態値 -
setState
: 状態を更新するための関数 -
initialValue
: 状態の初期値
状態の更新方法
// 直接値を設定
setState(newValue);
// 前の状態を使用して更新
setState(prevState => prevState + 1);
使用例
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>
増加
</button>
</div>
);
}
useStateの特徴
特徴 | 説明 |
---|---|
シンプル性 | 使い方が直感的で理解しやすい |
再レンダリング | 状態が変更されると自動的に再レンダリング |
非同期更新 | 状態更新は非同期で行われる |
上記の通り、useStateは状態と更新関数を直感的に使えるので非常に使いやすいです。
ただ、管理したい状態が少ない場合は何も問題はないのですが、複数の状態を管理したい場合はその分stateの定義が増えることになるので、メンテしにくくなってしまうというデメリットがあります(残念ながら実際の運用では状態管理が1つや2つで済むことのほうが稀です)。
そこで今回ご紹介するuseReducerの出番です!
こちらもuseStateと同じノリで以下の通りuseReducerをご紹介します↓
useReducerの基本構文
const [state, dispatch] = useReducer(reducer, initialState);
主要な要素
-
reducer
: 現在の状態とアクションを受け取り、新しい状態を返す関数 -
initialState
: 状態の初期値 -
state
: 現在の状態値 -
dispatch
: reducerを実行するための関数
特徴と利点
- ロジックとビューの分離が可能
- 複雑な状態管理に適している
- 状態の更新ロジックを一箇所で管理できる
useStateとの比較
特徴 | useState | useReducer |
---|---|---|
適用場面 | シンプルな状態管理 | 複雑な状態管理 |
更新方法 | 直接的な値の設定 | アクションを通じた更新 |
コード構造 | シンプル | より体系的 |
上の説明をご覧いただくとお分かりの通り、useStateは直感的に使えたんですが、useReducerの場合はそうはいかなそうです。
というわけで、useReducerの実際の使用例を、上記説明を踏まえて見てみることで理解を深めましょう!
useReducerの使用例
さて、ここでは以下の簡易アプリをuseReducerを使って実現してみることとします。
+を押下するとカウントアップ、-を押下するとカウントダウンするアプリ
※正直このぐらいの超簡易的なアプリならuseStateを使ってもよい(むしろuseStateの方が良いまである)のですが、今回は勉強ということでuseReducerを使って実装することとします。
こちら、まずは完成版のソースは以下の通りです。
// 初期値
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:
return state;
}
}
function Counter() {
// useReducerの定義
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}
useStateの場合と比較して何やら複雑そうなことをしていますね。。
ではこちら、ソースブロックごとに解説を入れてみましょう。
ちなみに、useStateを使って同じ機能を実現する場合はどのようになるかについても一応載せておきます↓
同じ機能をuseStateを使って実装したらこうなる
function Counter() {
//stateの定義
const [count, setCount] = useState(0);
// カウントアップ
const increment = () => {
setCount(prevCount => prevCount + 1);
};
// カウントダウン
const decrement = () => {
setCount(prevCount => prevCount - 1);
};
return (
<>
Count: {count}
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
}
export default Counter;
ソース解説
初期状態の定義
const initialState = { count: 0 };
まずは、useReducer関数の第二引数に渡す状態管理の初期値(カウンタの初期値0)を定義しておきます。ここはuseStateでも変わりませんね。
-
initialState
オブジェクトでカウンターの初期値を0に設定
Reducer関数
function reducer(state, action) {
switch (action.type) {
case 'increment': // カウントアップ
return { count: state.count + 1 };
case 'decrement': // カウントダウン
return { count: state.count - 1 };
default:
return state;
}
}
ここでは、useReducerの第一引数に渡すreducerを定義しています。
reducerとは、一言で言うと状態を更新するための関数
です(useStateで言うところのset~関数)。
ただ、useReducerの場合、更新関数はuseStateよりも厳密で、何でもかんでも値として設定できるようにされてはいません。
具体的には、第一引数には現在の状態、第二引数にはactionが渡された状態で実行され、returnされたオブジェクトが新しい状態として設定されます。
ここでは、+ボタンが押下されたら1カウントアップ、-ボタンが押下されたら1カウントダウンさせるように処理を実装しています(第二引数のaction.typeの値に応じて処理を分岐し、計算後の状態を設定した新しいオブジェクトをreturn)。
Reducer関数の役割
- 現在の状態(
state
)とアクション(action
)を受け取る - アクションのタイプに応じて新しい状態を返す
-
increment
: カウントを1増やす -
decrement
: カウントを1減らす
ここでは、あくまでもreducerは定義するだけで実行はしていません。
どのように実行するかについては以下を見てみましょう。
カウンターコンポーネント
function Counter() {
// useReducerの定義
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}
こちらでは、事前に定義したreducer、initialStateを引数として渡してやることでuseReducerの定義を行い、戻り値としてstate、dispatchを受け取っています。
文字通りstateでは状態を管理していますが(useStateと同じです)、見慣れないのはdispatchですね。こちらは、stateを更新するための関数(つまりreducer関数)
です。
上述の説明では、reducerは、第一引数に現在のstate、第二引数にactionが渡されて実行されるということでしたが、dispatchに対してactionのオブジェクトを渡して実行することで、内部的にreducerが呼び出されます。
(ここがややこしい)
つまり今回の例で言うと、加算したい場合は↑の通り、dispatch({ type: 'increment' })
、減算したい場合はdispatch({ type: 'decrement' })
と記述します。
こうすることで、reducer関数内でtypeの値に応じて処理が実行されることでstateの値が更新されるという仕組みですね。
状態更新の流れ
- ユーザーがボタンをクリック
-
dispatch
関数が呼び出される -
reducer
が新しい状態を計算 - コンポーネントが再レンダリング
ここで、なんでこんなことすんの?useStateでいいじゃん
と思うかもしれません(私は初見の時思いました)。
これは一言で言うと、状態管理を体系的に行うため
です。
例えば、useStateを使って状態管理を行う場合、カウンタの更新はsetCount関数を使って直接値を更新する方法になりますが、極端な話、setCount(prevCount => prevCount + 10000)
と書いてしまえばカウンタの状態を大きく変更できてしまいます。
カウンタは1ずつ変更したいのでこの使い方は本来NGなのですが、useStateの場合は指定した値に状態を更新してしまうので、こういった破壊的な変更ができてしまうということです。
一方、useReducerの場合はこういった要件を破壊するような変更はできません。
状態の更新は必ずdispatch(reducer)経由で行うので、1カウントアップするか、1カウントダウンさせるかの制御しかできないということです。
また、useReducerの場合、reducer関数をみれば、開発者が異なったとしても、どういう状態管理で、どんな時にどういう更新をするのか、が一目瞭然で分かるというメリットもあります(useStateの場合は更新関数が使い放題になるのでソースを全てチェックする必要が出てきてしまう)。
上述の通り、今回のような簡易アプリではuseReducerでなくても正直問題はないと思いますが、実際の運用では体系的に状態管理を行いたい局面も出てくると思います。
例えば、検索画面の検索フォームで状態管理したい場合などですね(私の個人開発中のアプリでもまさに検索フォームで使っています)。入力項目が複数で、かつ更新する方法が項目によって異なる場合、useStateで管理する場合は複数のstateを定義する必要がありますが、useReducerの場合は検索フォーム用にuseReducerを一つ定義し、typeで処理を分岐させることで解決できます。
こうして考えるとuseStateではなくuseReducerを使うことの意義というものも見えてくるのではないでしょうか。
おわりに
いかがだったでしょうか。
今回はuseReducerについての解説記事でした。
初見の時はなんでこんなめんどくさい状態管理があるんだよ
と思っていましたが、やっぱり自分でアプリを開発するとなると、今後の保守性のことも考えるようになるので、できるだけ後から見ても分かるように実装したくなりますよね(これが机上で勉強するのと実際にモノを作るときの意識の違いとして出てくる)。
最初はとっつきにくいことでも、一つ一つ理解していくとなんだこんなことか
と拍子抜けするというのはプログラミングあるあるなので、今後も新しい技術については自分なりに理解し、記事にしていきたいと思いますので、もしよかったらフォローいただければ幸いです!
参考
JISOUのメンバー募集中🔥
プログラミングコーチングJISOUではメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
気になる方はぜひHPからライン登録お願いします!👇