59
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React hooksでデータ取得をするuseFetchのちゃんとした実装

Last updated at Posted at 2019-04-22

はじめに

React 16.8でhooksが導入されましたが、Suspense for data fetchingやreact-cacheが登場するまでは、データ取得のベストプラクティスは曖昧です。しかし、キャッシュさえ重要でなければuseEffectで比較的簡単にFetch APIのフックを実装することができます。

実装

import { useEffect, useReducer } from 'react';

const initialState = {
  loading: false, // データ取得中はtrueに設定される
  error: null,    // データ取得でエラーになると設定される
  data: null,     // データ取得結果が設定される
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'init': // 初期状態に戻す
      return initialState;
    case 'start': // データ取得を開始する
      return { ...state, loading: true };
    case 'data': // データ取得が正常終了する
      return { ...state, loading: false, data: action.data };
    case 'error': // データ取得がエラー終了する
      return { ...state, loading: false, error: action.error };
    default: // それ以外は起こりえないのでバグ検知のためthrow
      throw new Error('no such action type');
  }
};

const defaultOpts = {}; // デフォルトでは空オプション
const defaultReadBody = body => body.json(); // デフォルトではjsonとしてparseする

export const useFetch = (
  url,
  opts = defaultOpts,
  readBody = defaultReadBody,
) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  useEffect(() => {
    let dispatchSafe = action => dispatch(action); // cleanupで無効にするため
    const abortController = new AbortController(); // cleanupでabortするため
    (async () => {
      if (!url) return;
      dispatchSafe({ type: 'start' });
      try {
        const response = await fetch(url, {
          ...opts,
          signal: abortController.signal,
        });
        if (response.ok) {
          const body = await readBody(response);
          dispatchSafe({ type: 'data', data: body });
        } else {
          const e = new Error(`Fetch failed: ${response.statusText}`);
          dispatchSafe({ type: 'error', error: e });
        }
      } catch (e) {
        dispatchSafe({ type: 'error', error: e });
      }
    })();
    const cleanup = () => {
      dispatchSafe = () => null; // we should not dispatch after unmounted.
      abortController.abort();
      dispatch({ type: 'init' });
    };
    return cleanup;
  }, [url, opts, readBody]);
  return state;
};

使い方

注意点は、useFetchの引数はuseEffectのdepsに渡されるため参照等価性が重要なことです。関数内で使う場合は、通常、useMemo等で包んでおく必要があります。

import React, { useMemo } from 'react';

const MyComponent = ({ url, token }) => {
  const opts = useMemo(() => ({
    headers: {
      Authorization: `Bearer ${token}`,
    },
  }, [token]);
  const { loading, error, data } = useFetch(url, opts);
  return (
    <div>
      {loading && <span>Loading...</span>}
      {error && <span>Failed to fetch</span>}
      {data && <div>{JSON.stringify(data)}</div>}
    </div>
  );
};

デモ

おわりに

本実装はAbortControllerを使ってコンポーネントのライフサイクルに合わせてfetchをキャンセルするため安全に使えます。一方で、キャッシュはないため、複数のコンポーネントで共通したデータ取得の場合も、それぞれで取得することになってしまいます。そのようなケースはreact-cacheに任せるとして、使い分けることになると思います。

追記 2021/08/20

dispatchのワーニングは、今後でなくなります(参考)。上記コードではdispatchSafeは不要になり、単にdispatchとするだけで良くなります。 嘘でした、新しいfetchが走った時に古いものが上書きしないように必要でした。

追記 2022/06/25

このuseFetchの実装はちゃんとはしているのですが、今ではベストプラクティスからは外れています。useEffectによるfetchの発火はレンダリング後になるためタイミングが遅い、かつ、無駄なレンダリングになる場合があるためです。Suspense指向の useFetch の実装としては例えば react-hooks-fetch をご参照ください。

59
50
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?