😓

データフェッチにはuseReducerが適している理由

3 min read

はじめに

以前にこんな記事を書いたが、基本的には多くのユースケースならuseStateの方が好きだ。useReducer(やRedux style)のように状態管理をラベリングしても最終的に実装を追わなければ全貌が把握できない。またaction名を考えるのは度々疲れを伴う。

しかしデータフェッチするHook(またはコンポーネント、HOC、Render prop)を実装するならReducerを噛ませる方針で良いんじゃないかと思った。そのように至った経緯は以下のようなもの。

データフェッチで管理するやつってお決まり

データフェッチの状態管理はロード中・成功・失敗の3つがあればデファクトスタンダードになることがもう十分分かっている時代である。また、Hookとして隠蔽するなら返り値はisLoading, data, errorといった具合になることも同様。

このように十分知られているものであれば、stateの更新がどうなるかは未知ではないので最初からreducerを作っていいだろう。

TypeScriptの推論を効かせたい...

実はここからが本題。useStateでfetchするhookを実装していた時に気が付いた。

import axios from "axios";
import { useEffect, useState } from "react";

type User = {
  id: number;
  name: string;
};

type IState = {
  users: User[];
  isLoading: boolean;
  error: Error | null;
};

const initialState: IState = {
  users: [],
  isLoading: false,
  error: null,
};

export default function useUsers() {
  const [state, setState] = useState<IState>(initialState);

  const fetchData = async () => {
    setState((state) => ({ ...state, isLoading: true }));

    try {
      const response = await axios.get<User[]>(`/api/users`);
      setState({ users: response.data, isLoading: false, error: null });
    } catch (error) {
      if (error instanceof Error) {
	// 型 'unknown' を型 'Error' に割り当てることはできません。
        setState((state) => ({ ...state, isLoading: false, error: error }));
      } else {
        throw error;
      }
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  return state;
}

errorブロックでは型を絞り込んでいるのに、この部分のsetStateは怒られてしまう。setState引数の関数内では型推論が行かないのだ。

結局こうすれば推論が適用されるが、どうも歯がゆい。

if (error instanceof Error) {
  const err = error;
  setState((state) => ({ ...state, isLoading: false, error: err }));
}

ここで閃いたが、errorを型推論されるような浅いレベル(何ていえばいいか分からない)に割り当てれば良いのでは?と思いsetStateからdispatchする形式にしてみた。結果Lintエラーは治った。

import axios from "axios";
import { useEffect, useReducer } from "react";

type User = {
  id: number;
  name: string;
};

type IState = {
  users: User[];
  isLoading: boolean;
  error: Error | null;
};

type IAction =
  | { type: "loading" }
  | { type: "success"; users: User[] }
  | { type: "error"; error: Error };

const initialState: IState = {
  users: [],
  isLoading: false,
  error: null,
};

const reducer = (state: IState, action: IAction): IState => {
  switch (action.type) {
    case "loading":
      return { ...state, isLoading: true };
    case "success":
      return { users: action.users, isLoading: false, error: null };
    case "error":
      return { ...state, isLoading: false, error: action.error };
    default:
      return state;
  }
};

export default function useUsers() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const fetchData = async () => {
    dispatch({ type: "loading" });

    try {
      const response = await axios.get<User[]>(`/api/users`);
      dispatch({ type: "success", users: response.data });
    } catch (error) {
      if (error instanceof Error) {
        dispatch({ type: "error", error: error });
      } else {
        throw error;
      }
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  return state;
}

Discussion

ログインするとコメントできます