はじめに
ReactHooksの1つにuseReducerがあります。useStateと同じように状態を管理できるフックです。
結構複雑なので、私は苦手意識を持っています。
改めて基本構造を確認し、現場で使われる実践的なものがどのようなものかを確認していきます。
useReducerの基本構造
基本的には以下の形で使用します。
const [state, dispatch] = useReducer(reducer, initialArg, init?)
戻り値
state:
現在の状態が入っています。
useStateでのstateと同じです。
dispatch:
stateを更新するための関数です。
この関数は引数としてactionを設定する必要があります。
一般的にはactionにはtypeというプロパティを持ったオブジェクトを設定します。
このtypeプロパティによって処理を分岐させます。
引数
reducer:
stateをどのように更新するかを指定するための関数です。
ここで指定した関数が、dispatchを実行した際に呼び出されます。
initialArg:
stateの初期値です。
init:
省略可能な関数です。
stateを初期化するための関数になります。
基本的にはinitialArgで初期値を指定することが多いかと思います。
よくみるuseReducerの使用例
import { useReducer } from "react";
const initialState = { count: 0 };
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
};
const MyComponent = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<div>Count: {state.count}</div>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</div>
);
};
export default MyComponent;
画面上の値をカウントアップ、カウントダウンする処理がよく使われているのでは、と思っています。
この例ではreducerの処理を、action.typeによって加算、減算で分岐させています。
それぞれのdispatchにtypeを指定することで、実現したい処理をonClickに設定しています。
加算減算の処理はreducerという外部の関数に切り出し、適切なアクションをdispatchを通じて渡すことで処理を実行する、というuseReducerの流れがよくまとまっている良い例かと思います。
しかし、この程度であればuseStateを使っても同じことができます。
この例ではuseReducerのメリットをあまり感じることができませんね。
では、実際の現場ではどのようなコードが使われているのでしょうか。
実務で見かけたuseReducerの使用例
コード例
業務のコードをそのまま出すわけにはいかないので、あくまでも用途だけを真似した形で、実際のコードは生成AIを通じて作ってもらいました。
import { useReducer } from "react";
const initialState = {
isAuthenticated: false,
user: null,
error: null,
};
const reducer = (state, action) => {
switch (action.type) {
case "LOGIN_SUCCESS":
return { isAuthenticated: true, user: action.payload, error: null };
case "LOGIN_FAILURE":
return { ...state, error: action.payload };
case "LOGOUT":
return initialState;
default:
return state;
}
};
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const [shouldSucceed, setShouldSucceed] = React.useState(true);
const handleLogin = () => {
shouldSucceed
? dispatch({
type: "LOGIN_SUCCESS",
payload: { id: 1, name: "テスト", role: "admin" },
})
: dispatch({ type: "LOGIN_FAILURE", payload: "ログインに失敗しました" });
};
return (
<>
{state.isAuthenticated ? (
<>
<div>ようこそ、{state.user.name}さん</div>
{state.user.role === "admin" && <div>管理者権限があります</div>}
<button onClick={() => dispatch({ type: "LOGOUT" })}>
ログアウト
</button>
</>
) : (
<>
<button onClick={handleLogin}>ログイン</button>
{state.error && <div style={{ color: "red" }}>{state.error}</div>}
<label>
<input
type="checkbox"
checked={shouldSucceed}
onChange={(e) => setShouldSucceed(e.target.checked)}
/>
ログイン成功
</label>
</>
)}
</>
);
};
export default App;
ログインを管理するための処理です。
動きを確認するため、チェックボックスON/OFFでログイン処理成功と失敗を切り替えられるようにしています。
実際の画面の動き
実際の画面は以下のようになります。
ログイン処理成功の状態でログイン処理を行います。
ログイン完了後の画面が表示されます。
(簡易化のため実際のログイン処理はだいぶ省略しています。)
ログアウトボタンを押下すればはじめの画面に戻ります。
また、ログイン成功をOFFにした場合
ログイン処理が失敗した想定の画面が表示されます。
コード解説
実際の業務で見た画面と似たような構成でコード例について簡単に解説します。
まず、ポイントはログインステータスをactionとして設定している点です。
これにより、ログイン、ログアウト、ログイン失敗のそれぞれでどのような動きをさせるか、をreducerに定義しています。
また、権限やメッセージなどもactionとして渡すことで、それぞれのログインステータスに応じた処理をさせることができています。
そして、設定されたactionやstateを元に、表示させるコンポーネントを切り替えることで擬似的にログイン処理を実現しています。
これらの動きをuseStateで実現させるのはかなり難しそうです。
まとめ
今回は実際の現場で私が見たuseReducerのコードを通じて、どんなふうに使われているのかを確認しました。
useReducerは今回のようなログイン状態、ユーザー情報、エラーメッセージなど、関連する状態を管理する場合には非常に有効なようです。
また、他にもuseContextと組み合わせることによってさらにいろいろな挙動を実現させることができるようですが、これに関してはReduxという別のライブラリを使うほうがもっと便利だ、というような話もあるので、こちらのライブラリについてもそのうち確認してみたいと思いました。