⚛️

useReducerでState管理をスッキリさせてみる

2022/09/25に公開

こんにちは👋

今回は、ごちゃごちゃしているState管理に対してuseReducerを使うことでスッキリさせてみようと思います。

useReducerとは?

今回はuseReducerの使い方自体がメインではないので、ざっくりと文章で説明しますが、useReducerではstateの更新をdispatch関数で行います。dispatchにはオブジェクトを渡すことができ、渡されたオブジェクトはreducer関数に渡ります。そして、reducerが渡されたオブジェクトをもとに新しいstateを生成して返すことでstateの更新が行われます。

reducer関数は自身で定義するため、関連性のあるstateをまとめ、stateの更新処理をスッキリさせることが可能になります。

https://ja.reactjs.org/docs/hooks-reference.html#usereducer

State管理がごちゃごちゃしている例

それではさっそく本題へ。

今回は「ボタンをクリックしてAPIからデータを取得して画面に取得したデータの情報を表示する」という処理を実装するケースを考えます。

「データ取得中はローディング中を示すコンポーネントやメッセージを使いたいな」とか「エラー時にはエラーメッセージを表示させたい」ということを考えると、ローディングフラグ管理、エラーフラグ管理に加えて取得したデータを管理する用のstateが必要になるかなと思います。

そこで、最初の例ではuseStateでそれぞれのstateを分けて管理する方法で実装を行いたいと思います。

App.tsx
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で問題なく実装することができましたが、isLoadingpostisErrorという3つのstateを別々に管理するとなるとどこかでstateの更新漏れが起こりそうですし、コードの可読性にも書けるかなと思います。

useReducerでState管理をスッキリさせる

それでは、useReducerを使用して書き換えたいと思うので、まずはreducer関数としてpostReducer()を定義したいと思います。

postReducer.ts
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の種類によってisLoadingisErrorpostそれぞれのstateの処理をまとめて変更しています。これによってuseStateでバラバラに管理していたstateをまとめて管理できるようになりました。さらに、コンポーネントファイルから複雑なロジックを分離させることができたため、コードの見通しも良くなりました。

では続いて、App.tsxの中身も書き換えてみましょう。

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管理をスッキリさせてみた話でした。

GitHubで編集を提案

Discussion