はじめに
Reactの状態管理のためのフックとして、useStateとuseReducerの2つがあります。
2つの違いがいまいちピンときておらず、なんとなくuseStateを使えばいいのかな、と思っていました。
なんとなくの理解から確実に自分の知識とするために、それぞれの詳細を確認し、その違いを理解していきます。
useState
基本の形
const [state, setState] = useState(init);
引数
init:
stateに設定したい初期値。どんな型の値でもよい。関数も可。
戻り値
戻り値は配列で渡されるため、受け取る際は分割代入で受け取るのが一般的。
また、その際の変数名は何でもよいが、状態xxxxに対して、関数はsetXxxxとするのが一般的らしい。
state:
現在の状態。一番初めはinitで設定した値が入る。
initに関数を設定した場合は、その関数の戻り値が入る。
setState:
新しい状態を設定するための関数。
通常、引数には新しい状態を直接指定する。
前の状態に基づいて新しい状態を設定する場合には、関数を渡すこともできる。
関数を渡す場合は以下
setState(prev => {
// do something
return newState;
});
setStateにわたす関数には、前の状態(prev)が引数として渡される。これも名前は何でもいい。
そして、新しく設定したい状態をreturnとして返す。
また、setStateを呼び出すとコンポーネントが再レンダリングされる。
使用例
import { useState } from "react";
const Example = () => {
const [count, setCount] = useState(0);
const countUp = () => setCount(prev => prev + 1);
const countDown = () => setCount(prev => prev - 1);
return (
<>
<h3>{count}</h3>
<button type="button" onClick={countUp}>count up</button>
<button type="button" onClick={countDown}>count down</button>
</>
);
};
export default Example;
[count up]ボタンを押すたび、画面上の値が1ずつインクリメントされ、[count down]ボタンを押すと画面上の値がデクリメントされます。
useReducer
基本の形
const [state, dispatch] = userReducer(reducer, init);
引数
reducer:
stateを更新するための関数。
この関数の第一引数には、現在の状態が渡される。第二引数には後述のdispatchに渡した引数が設定される。
新しいstateに設定したい値を戻り値として返すようにする。
const reducer = (state, action){
switch(action.type) {
case anything:
return newState;
}
}
init:
stateに設定したい初期値。
useReducerは第三引数として、初期値を生成する関数を設定できますが、省略可能なためここでは割愛します。
戻り値
状態管理ライブラリであるReduxの影響を受けているらしい。
そのため、更新用関数の変数名はdispatchとするのが一般的
state:
現在の状態。一番初めはinitで設定した値が入る。
dispatch:
引数で設定したreducerを呼び出す。引数に何を設定してもよいが、オブジェクトで渡すのが一般的。
プロパティとしてtypeを設定し、それによって処理方法を分岐する。
使用例
import { useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case '+':
return state + 1;
case '-':
return state - 1;
default:
return state;
}
};
const Example = () => {
const [count, dispatch] = useReducer(reducer, 0);
const countUp = () => dispatch({ type: '+' });
const countDown = () => dispatch({ type: '-' });
return (
<>
<h3>{count}</h3>
<button type="button" onClick={countUp}>count up</button>
<button type="button" onClick={countDown}>count down</button>
</>
);
};
export default Example;
実行結果はuseStateのときと同じ
2つの共通点
useState, useReducerの両者とも
- 現在の状態を持つ
- 状態を更新する
- 状態の更新時、コンポーネントを再レンダリングする
ということが共通しています。
そのため、画面の操作によって画面上の値などを変化させたい場合、どちらを使ってもよいように思えます。
2つの相違点
決定的な違いは、更新の方法を決めるタイミングです。
useStateの場合
useStateの戻り値である更新用関数のsetStateを実行するときにstateをどのように更新するかを決定します。
つまり、コンポーネントの内部で更新方法を決定します。
useReducerの場合
useReducerにreducerとして関数を渡し、stateをどのように更新するかを決定します。
reducerはコンポーネントの外部で設定することも可能です。
useReducerの戻り値である更新用の関数dispatchを実行する際には、すでにreducerによって更新方法が決定されているため、単にアクションを引数として渡すだけです。
どちらを使うべきか
基本的にはuseStateでよいと考えています。
しかし、複雑なロジックや、互いに関連する状態を管理する場合にはuseReducerのほうがわかりやすくなると思います。
以下のように、入力された項目と、カウント数を表示する画面があるとします。

これをuseReducerを使って表現すると以下のようになります。
import { useReducer } from "react";
const ACTIONS = {
PLUS: 'PLUS'
, MINUS: 'MINUS'
, CHANGE_TXT: 'CHANGE_TXT'
}
const INIT_DATA = { text: 'apple', count: 0 };
const reducer = (state, action) => {
switch (action.type) {
case 'PLUS':
return { ...state, count: state.count + 1 };
case 'MINUS':
return { ...state, count: state.count - 1 };
case 'CHANGE_TXT':
return { ...state, text: action.text };
default:
return state;
}
};
const Example = () => {
const [state, dispatch] = useReducer(reducer, INIT_DATA);
const countUp = () => dispatch({ type: ACTIONS.PLUS });
const countDown = () => dispatch({ type: ACTIONS.MINUS });
const changeText = (e) => dispatch({ type: ACTIONS.CHANGE_TXT, text: e.target.value })
return (
<>
<h3>{state.text}:{state.count}</h3>
<button type="button" onClick={countUp}>+</button>
<button type="button" onClick={countDown}>-</button>
<input type="text" value={state.text} onChange={changeText}></input>
</>
);
};
export default Example;
カウントの増減、テキストの変更の処理がすべてreducerの中にまとまっており、シンプルです。
同じ処理をuseStateを使って実装すると、更新用関数であるsetStateの呼び出し時に処理を記載することになるため、処理がバラバラの場所に記載されます。
まとめ
useStateとuseReducerについて、基本的には実現できることは変わらないことがわかりました。
通常はだいたいuseStateでよさそうですが、複雑な処理や複数の状態を管理する場合には、useReducerの方がより理解しやすいといえます。