はじめに
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のワーニングは、今後でなくなります(参考)。上記コードでは 嘘でした、新しいfetchが走った時に古いものが上書きしないように必要でした。dispatchSafe
は不要になり、単にdispatch
とするだけで良くなります。
追記 2022/06/25
このuseFetchの実装はちゃんとはしているのですが、今ではベストプラクティスからは外れています。useEffectによるfetchの発火はレンダリング後になるためタイミングが遅い、かつ、無駄なレンダリングになる場合があるためです。Suspense指向の useFetch
の実装としては例えば react-hooks-fetch をご参照ください。