カスタムフックの使い方 – useFetchを例に解説

Reactには、useState、useEffectなどに代表されるフック(関数)が幾つかありますが、カスタムフックと呼ばれるフックを自作することも可能です。本稿ではReactのカスタムフックのうち、GETリクエストによく用いられるカスタムフックであるuseFetchの書き方・使い方をご紹介します。カスタムですのでフック(関数)の名称も任意ですが、useFetchについては慣例でこの名前が用いられることが多いように思います。中身は作り手によって少しづつ異なります。以下カスタムフックであるuseFetchを使わない例を先にご紹介し、続いて同じ機能をuseFetchを使って実装する例を解説いたします。

fetchによるGETリクエスト

カスタムフックを使わずにfetchでGETリクエストをすると下記のようになります。以下順に説明いたしますが、不要な方は飛ばして次節のカスタムフックの説明をご覧ください。

FetchExample.jsx

import { useState, useEffect } from "react";

function FetchExample() {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const url = "https://jsonplaceholder.typicode.com/todos";

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const data = await response.json();
setData(data);
setIsLoading(false);
} catch (error) {
setError(error);
setIsLoading(false);
}
};
fetchData();
//eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (isLoading) {
return <h4>Loading...</h4>;
}

if (error) {
alert(error);
return <h4>Error Occured</h4>;
}

return (
<div>
{data.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
);
}

export default FetchExample;

useState

FetchExampleの内部で3つのuseStateを設定しています。一つ目はisLoadingです。外部APIへのリクエスト中はisLoadingをtrueとし、レスポンスを得たら、あるいはエラーが起きたらfalseとしています。このisLoadingの効果的な使い方については私の別の記事「くるくる回るローディング画面の実装方法 – Reactなら簡単」もご覧ください。2つ目はerrorです。リクエストでエラーが生じたときにその内容を格納します。3つ目はレスポンスのデータを格納するdataです。また、本稿ではJSONPlaceholderという無料のFree fake APIを利用しています。うち今回使うtodosエンドポイントでは、ラテン語でtodoが200件返ってきます。

useEffect

useEffectの中でGETリクエストをしています。リクエストは非同期なので、全体を非同期処理の構成にします。.thenで繋いでもいいですが、本事例ではasync awaitの構成をとっています。残念ながらuseEffectそのものはasyncにすることが出来ないので、useEffectの内部にasyncの関数を作り、同関数を最後に実行するという構成にする必要があります。本事例ではfetchDataがその関数です。

useEffectの中のtryブロック

リクエストではエラーが生じ得るので、リクエスト本体をtryし、エラーをcatchします。tryの1行目がリクエスト本体、2行目でレスポンスをjsonに変換してdataに格納します。3行目で得られたdataをlocal stateのdataに格納し、4行目ではリクエストを終えましたのでisLoadingをfalseにしています。

useEffectの中のcatchブロック

catchブロックではエラーを処理します。エラーの内容をlocal stateのerrorに格納、続いてエラーの場合もローディングは完了していますのでisLoadingをfalseにします。

eslint-disable-next-lineのコメント

本事例では、初回レンダリング時にのみリクエストをすればいいので、usEffectの第二引数のdependecy array(従属配列)は空にします。ただuseEffectの内部でuseState変数を使用する一方で、dependency arrayを空にするとエラーが出るので、下記コメントを入れることでエラーを回避します。

//eslint-disable-next-line react-hooks/exhaustive-deps

map関数を使用したレスポンスの展開

isLoadingの処理は前述の別の記事をご参照ください。また、error処理は内容をalertしつつ、エラーが生じた旨をh4で表示する簡易なものとしています。上記以外、すなわち正常にレスポンスのtodosが得られた場合の処理を、最後のブロックで実施しています。dataに格納されたtodosは多数ありますので、map関数でそれぞれの要素(todo)に対して共通の処理、ここではdivタグにtodoの内容を記述する処理を実行します。map関数がtodoの内容が書かれた複数の(多数の)divタグを返す結果、todoが一覧表示されます。

以上がカスタムフックを使わない場合の処理です。GETリクエストがアプリ内で1つならこのままでいいですが、1つのプロジェクト内で複数のAPIエンドポイントにGETリクエストをする場合、毎回上記を書くのはやや非効率です。そこで上記処理をまとめて一つのフック(関数)とし、使いまわせるようにしたのがカスタムフックです。

useFetchを例にしたカスタムフックの作り方

カスタムフックでは、returnの部分でHTML(JSX)を返す代わりに状態(local state)を返します。残りの大部分のコードは上述のコンポーネントに直接書いた内容と同じです。具体的には下記のようになります。

useFetch.js

import { useState, useEffect } from "react";

function useFetch(url, config) {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState(null);

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url, config);
const data = await response.json();
setData(data);
setIsLoading(false);
} catch (error) {
setError(error);
setIsLoading(false);
}
};
fetchData();
//eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return { data, loading, error };
}

export default useFetch;

return以外の大きな違いは、useFetchフックが引数をとる点です。useFetchは複数のURL、APIエンドポイントに対するリクエストで使いたいのでfetchの引数であるurlとconfigurationのオブジェクトをuseFetchフックの引数としています。他は、FetchExample.jsxと同じです。

通常の関数とカスタムフックの違い

通常の関数と、カスタムフックの違いは通常の関数が内部にReactのフックを持たないのに対して、カスタムフックは内部にReactのフックを持つ(内包する)点です。本事例のカスタムフックuseFetchからの戻り値は、いずれもuseStateの変数ですが、これらはそのままインポート先のコンポーネントで使うことができます。反対に内部にフックを持たない関数はカスタムフックとはなりません。その他、カスタムフックの慣例上の決まりとして、カスタムフックはuseXXXXという呼称にします。

useFetchをインポートするコンポーネント側の処理

次にuseFetchを利用するコンポーネント側の処理です。本事例ではまず冒頭でuseFetchをインポートします。続いて、FetchComponent(関数コンポーネント)の2行目でこのuseFetchを実行しています。第二引数は、本事例では不要なので空のオブジェクトとしています。戻り値、data、loading、errorはいずれもuseStateの変数で、このコンポーネント内ではuseStateを直接インポートせずともそれら変数を使うことができます(最もカスタムフックらしい部分)。return以下は、先ほどのカスタムフックを使わない事例と同じです。

FechComponent.jsx

import useFetch from "../hooks/useFetch";

function FetchComponent() {
const url = "https://jsonplaceholder.typicode.com/todos";
const { data, loading, error } = useFetch(url, {});

if (isLoading) {
return <h4>Loading...</h4>;
}


if (error) {
alert(error);
return <h4>Error Occured</h4>;
}

return (
<div>
{data.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
);
}

export default FetchComponent;

このようにカスタムフックを定めておくと、別のコンポーネントで別のAPIエンドポイントにリクエストをする場合でも上記のようにすっきりと記述することができます(useFetchをインポートするだけ)。

本事例ではfetchを使いましたが、より高機能なfetchライブラリーであるaxiosのメリットや使い方については、私のこちらの記事「Reactでのaxiosの使い方【外部APIの取得方法】」で詳しく解説しています(カスタムフックにはしていませんが)。宜しければ併せてご覧になってみてください。

カスタムフックを日本語で学べる講座

カスタムフックについては、本稿と併せてUdemyのこちらの講座「Reactに入門した人のためのもっとReactが楽しくなるステップアップコース完全版」の受講をお勧めいたします。このコースでは本稿と同じ外部APIにリクエストをしていますが、fetchではなくaxiosを使っている点、またtry catch構造ではなく.thenで繋いでる点が本稿のカスタムフックとは異なり補完的です。その他本コースはReactの応用編という位置付けで、Router、useMemo、useCallbackなどのレンダリング最適化、グローバル状態管理ではContextとRecoil、カスタムフック、ChakuraUI、それにTypeScriptの入門まで、講師の岡田さん大変分かりやすく説明されております。宜しければ併せてご覧になってみてください。


今日も最後まで読んで頂きありがとうました。