通常
コンポーネントに必要なデータを取得する際はuseEffect
を用いて記述します。
const useData = <T>(url: string): T | null => {
const [data, setData] = useState<T | null>(null);
useEffect(() => {
let ignore = false;
fetch(url, { signal: abortController.signal })
.then((res) => res.json())
.then((data) => {
if (!ignore) {
setData(data);
}
})
.catch(reportError);
return () => {
ignore = true;
};
}, [url]);
return data;
};
もしくは
const useData = <T>(url: string): T | null => {
const [data, setData] = useState<T | null>(null);
useEffect(() => {
const abortController = new AbortController();
fetch(url, { signal: abortController.signal })
.then((res) => res.json())
.then((data) => setData(data))
.catch((error) => {
if (error.name === 'AbortError') return;
reportError(error);
});
return () => {
abortController.abort();
};
}, [url]);
return data;
};
記述の違いは競合状態の解決方法になります。その記述がない場合は以下のようになります。
const useData = <T>(url: string): T | null => {
const [data, setData] = useState<T | null>(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => setData(data))
.catch(reportError);
}, [url]);
return data;
};
初期マウント時とurl
が変更されたタイミングの描画後に、useEffect
に記述した内容が実行されて、fetch
で獲得した情報をdata
に詰め込んでいます。
data
はuseData
の返り値でデータの取得前はnull
を返して、取得して詰め込まれた後はT
を返すようになっています。
このままでは、fetch
が完了する前に次のuseEffect
が呼び出される場合に、前に呼び出したfetch
よりも後に呼び出されたfetch
が早く帰ってきて、data
に古いfetch
の結果が詰め込まれるので注意する必要があります。最初に紹介したコードはその対策がされていました。
1つ目のコードはクリーンアップ時にuseEffect
内のローカル変数ignore
をtrue
にすることで、前に実行されたfetch
の結果がdata
へ詰め込まれないようにしました。
2つ目のコードはAbortController
を用いてfetch
にsignal
を渡して、クリーンアップ時にAbortController
をabort
するようにしました。これによって前に実行されたfetch
の結果がAbortError
となってfetch
がキャンセルされて、data
への更新が行われなくなります。
2つ目のコードはfetch
自体をキャンセルするので無駄なリクエストが続かない点でメリットがありますが、IEへの対応が必要な場合はAbortController
が使えないので注意する必要があります。
Suspense
、ErrorBoundary
を利用する
Suspense
やErrorBoundary
を使ってコードに境界を設けている場合はもう少しシンプルに書けます。
こちら条件ではフレームワークやライブラリを用いてデータの取得を行うことが多く、スタンダードな方法がないので、あくまで私が良いと考えている方法での実装となります。
const cacheMap = new Map<string, unknown>();
const useData = <T>(url: string): T => {
const cacheData = cacheMap.get(url) as T | undefined;
if (cacheData === undefined) {
throw fetch(url)
.then((res) => res.json())
.then((data) => cacheMap.set(url, data));
}
return cacheData;
};
Suspense
境界で捕捉されるのはthrow
されたPromise
オブジェクトなので、fetch
を投げる用に記述しています。cacheMap
を作成して結果をキャッシュさせているのは、再レンダリングが発火することによるuseData
の再計算でfetch
が再度投げられるのを防ぐためです。
このキャッシュ部分は、ReactのCanaryバージョンの世界ではcache
を使ったり、フレームワークやライブラリではstale-while-revalidate
が用いられてたり、より良いデータ取得のために作り込まれることが多いです。
これは以下のように利用します。
const Sample = () => {
const data = useData(url);
return <Data data={data} />;
}
利用する際は最低でもルートで以下のように境界付けを行うようにしてください。
<ErrorBoundary>
<Suspense>
<Sample />
</Suspense>
</ErrorBoundary>
canaryバージョン
ReactのCanaryリリースされているuse
を用いることで自作することなくデータの取得を行えます。こちらもSuspense
やErrorBoundary
を使ってコードに境界を設ける必要があります。
const Sample = () => {
const data = use(fetchData);
return <Data data={data} />;
};
use
は他のhooksとは異なり、if文中などに記述することもできますが、呼び出す場所はレンダリング時に計算される場所でなくてはいけません(イベントハンドラ内には記述できません)。