useReducerでState管理をスッキリさせてみる
こんにちは👋
今回は、ごちゃごちゃしているState管理に対してuseReducer
を使うことでスッキリさせてみようと思います。
useReducerとは?
今回はuseReducer
の使い方自体がメインではないので、ざっくりと文章で説明しますが、useReducer
ではstateの更新をdispatch
関数で行います。dispatch
にはオブジェクトを渡すことができ、渡されたオブジェクトはreducer
関数に渡ります。そして、reducer
が渡されたオブジェクトをもとに新しいstateを生成して返すことでstateの更新が行われます。
reducer
関数は自身で定義するため、関連性のあるstateをまとめ、stateの更新処理をスッキリさせることが可能になります。
State管理がごちゃごちゃしている例
それではさっそく本題へ。
今回は「ボタンをクリックしてAPIからデータを取得して画面に取得したデータの情報を表示する」という処理を実装するケースを考えます。
「データ取得中はローディング中を示すコンポーネントやメッセージを使いたいな」とか「エラー時にはエラーメッセージを表示させたい」ということを考えると、ローディングフラグ管理、エラーフラグ管理に加えて取得したデータを管理する用のstateが必要になるかなと思います。
そこで、最初の例ではuseState
でそれぞれのstateを分けて管理する方法で実装を行いたいと思います。
import { useState } from "react";
type Post = {
userId?: string;
id?: string;
title?: string;
body?: string;
};
export const App = () => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [post, setPost] = useState<Post>({});
const [isError, setIsError] = useState<boolean>(false);
const handleFetch = () => {
setIsLoading(true);
setIsError(false);
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((res) => {
return res.json();
})
.then((data) => {
setPost(data);
setIsLoading(false);
})
.catch((err) => {
setIsError(true);
setIsLoading(false);
});
};
return (
<>
<button onClick={handleFetch}>
{isLoading ? "データ取得中..." : "データを取得する"}
</button>
<p>{post?.title}</p>
<span>{isError && "エラーが発生しました"}</span>
</>
);
};
CodeSandboxで実装
というわけで、useState
で問題なく実装することができましたが、isLoading
、post
、isError
という3つのstateを別々に管理するとなるとどこかでstateの更新漏れが起こりそうですし、コードの可読性にも書けるかなと思います。
useReducerでState管理をスッキリさせる
それでは、useReducer
を使用して書き換えたいと思うので、まずはreducer
関数としてpostReducer()
を定義したいと思います。
import { PostState } from "./types/post";
import { PostAction, postActionKind } from "./types/postAction";
export const INITIAL_STATE = {
isLoading: false,
isError: false,
post: {}
};
export const postReducer = (state: PostState, action: PostAction) => {
switch (action.type) {
case postActionKind.FETCH_START:
return {
isLoading: true,
isError: false,
post: {}
};
case postActionKind.FETCH_SUCCESS:
return {
...state,
isLoading: false,
isError: false,
post: action.payload
};
case postActionKind.FETCH_ERROR:
return {
isLoading: false,
isError: true,
post: {}
};
default:
return state;
}
};
型定義ファイルを分けたので分かりにくい部分もあるかもしれませんが、postReducer
で行うこととしては、dispatch
を経由して渡されたaction
の種類によってisLoading
、isError
、post
それぞれのstateの処理をまとめて変更しています。これによってuseState
でバラバラに管理していたstateをまとめて管理できるようになりました。さらに、コンポーネントファイルから複雑なロジックを分離させることができたため、コードの見通しも良くなりました。
では続いて、App.tsx
の中身も書き換えてみましょう。
import { useReducer } from "react";
import { INITIAL_STATE, postReducer } from "./postReducer";
import { postActionKind } from "./types/postAction";
export const App = () => {
const [state, dispatch] = useReducer(postReducer, INITIAL_STATE);
const handleFetch = () => {
dispatch({ type: postActionKind.FETCH_START });
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((res) => {
return res.json();
})
.then((data) => {
dispatch({ type: postActionKind.FETCH_SUCCESS, payload: data });
})
.catch((err) => {
dispatch({ type: postActionKind.FETCH_ERROR });
});
};
return (
<>
<button onClick={handleFetch}>
{state.isLoading ? "データ取得中..." : "データを取得する"}
</button>
<p>{state.post?.title}</p>
<span>{state.isError && "エラーが発生しました"}</span>
</>
);
};
CodeSandboxで実装
stateの更新処理を先程のファイルに分けたので、このファイルで行うことはuseReducer
から返されたdispatch
関数を使用して、stateの更新を行いたい場所で行いたい処理に対応したactionタイプやデータを渡すだけになります。
これで修正前のuseState
でゴリ押ししていたパターンと比較するとコードの見通しが良くなったかと思います。
最後に
駆け足で説明したので不足している部分もあるかもしれませんが、useReducer
の使い所なんかが少し分かってきたのではないでしょうか。「stateをどこまでまとめて管理して良いのか」という判断も重要になってくるとは思いますが、上手く使うことでコードの見通しが結構改善されると思うので、「そういえば、こういうときuseReducer
を使えばコードをもっとスッキリさせることができたような?」みたいな感じでも良いので、頭の片隅に置いておくとどこかで役に立つ日が来るかも...!?
以上、useReducer
を使用してState管理をスッキリさせてみた話でした。
Discussion