データをフェッチしてキャッシュするためのカスタムReactフックを作成する方法

公開: 2022-03-10
簡単な要約↬Reactアプリケーションの多くのコンポーネントは、ユーザーに表示されるデータを取得するためにAPIを呼び出さなければならない可能性が高いです。 componentDidMount()ライフサイクルメソッドを使用してこれを行うことはすでに可能ですが、フックの導入により、データをフェッチしてキャッシュするカスタムフックを構築できます。 それがこのチュートリアルでカバーするものです。

React Hooksの初心者の場合は、公式ドキュメントを確認して把握することから始めることができます。 その後、ShedrackAkintayoの「ReactHooksAPI入門」を読むことをお勧めします。 あなたが確実にフォローしていることを確認するために、Adeneye David Abiodunによって書かれた、ReactHooksのベストプラクティスをカバーする記事もあります。

この記事全体を通して、Hacker News Search APIを使用して、データのフェッチに使用できるカスタムフックを構築します。 このチュートリアルではHackerNews Search APIについて説明しますが、フックは、渡された有効なAPIリンクから応答を返すように機能します。

ベストリアクトプラクティス

Reactは、豊富なユーザーインターフェイスを構築するための素晴らしいJavaScriptライブラリです。 これは、インターフェースを適切に機能するコードに編成するための優れたコンポーネントの抽象化を提供し、それを使用できるものはほぼすべてあります。 Reactの関連記事を読む→

Reactコンポーネントでのデータのフェッチ

Reactフックの前は、componentDidMount()ライフサイクルメソッドで初期データをフェッチし、 componentDidMount() componentDidUpdate()サイクルメソッドでpropまたは状態の変更に基づいてデータをフェッチするのが一般的でした。

仕組みは次のとおりです。

 componentDidMount() { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=JavaScript` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } componentDidUpdate(previousProps, previousState) { if (previousState.query !== this.state.query) { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${this.state.query}` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } }

componentDidMountライフサイクルメソッドは、コンポーネントがマウントされるとすぐに呼び出されます。それが完了すると、Hacker News APIを介して「JavaScript」の検索をリクエストし、応答に基づいて状態を更新します。

一方、 componentDidUpdateライフサイクルメソッドは、コンポーネントに変更があったときに呼び出されます。 状態に「データ」を設定するたびにメソッドが呼び出されないように、状態の前のクエリを現在のクエリと比較しました。 フックを使用することで得られることの1つは、両方のライフサイクルメソッドをよりクリーンな方法で組み合わせるということです。つまり、コンポーネントのマウント時と更新時の2つのライフサイクルメソッドを用意する必要はありません。

ジャンプした後もっと! 以下を読み続けてください↓

useEffectフックを使用したデータのフェッチ

useEffectフックは、コンポーネントがマウントされるとすぐに呼び出されます。 いくつかのpropまたは状態の変更に基づいてフックを再実行する必要がある場合は、それらを依存関係配列( useEffectフックの2番目の引数)に渡す必要があります。

フックを使用してデータをフェッチする方法を見てみましょう。

 import { useState, useEffect } from 'react'; const [status, setStatus] = useState('idle'); const [query, setQuery] = useState(''); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]);

上記の例では、 useEffectフックへの依存関係としてqueryを渡しました。 そうすることで、クエリの変更を追跡するようにuseEffectに指示します。 前のquery値が現在の値と同じでない場合、 useEffectが再度呼び出されます。

そうは言っても、必要に応じてコンポーネントにいくつかのstatusを設定します。これにより、いくつかの有限状態statusに基づいて、画面にメッセージをより適切に伝えることができます。 アイドル状態では、検索ボックスを使用して開始できることをユーザーに知らせることができます。 フェッチ状態では、スピナーを表示できます。 そして、フェッチされた状態で、データをレンダリングします。

fetched済みステータスを設定する前にデータを設定することが重要です。これにより、 fetched済みステータスの設定中にデータが空になった結果として発生するちらつきを防ぐことができます。

カスタムフックの作成

「カスタムフックは、名前が「use」で始まるJavaScript関数であり、他のフックを呼び出す可能性があります。」

— ReactDocs

それがまさにそれであり、JavaScript関数とともに、アプリのいくつかの部分でコードの一部を再利用することができます。

React Docsからの定義はそれを与えましたが、カウンターカスタムフックを使用して実際にどのように機能するかを見てみましょう。

 const useCounter = (initialState = 0) => { const [count, setCount] = useState(initialState); const add = () => setCount(count + 1); const subtract = () => setCount(count - 1); return { count, add, subtract }; };

ここには、オプションの引数を取り、値を状態に設定し、更新に使用できるaddメソッドとsubtractメソッドを追加する通常の関数があります。

カウンターが必要なアプリのどこでも、通常の関数のようにuseCounterを呼び出し、 initialStateを渡すことができるため、どこからカウントを開始するかがわかります。 初期状態がない場合、デフォルトで0になります。

実際の動作は次のとおりです。

 import { useCounter } from './customHookPath'; const { count, add, subtract } = useCounter(100); eventHandler(() => { add(); // or subtract(); });

ここで行ったのは、宣言したファイルからカスタムフックをインポートして、アプリで使用できるようにすることでした。 初期状態を100に設定したため、 add()を呼び出すたびにcountが1増加し、 subtract()を呼び出すたびにcountが1減少します。

useFetchフックの作成

簡単なカスタムフックを作成する方法を学習したので、ロジックを抽出してデータをカスタムフックにフェッチしましょう。

 const useFetch = (query) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]); return { status, data }; };

queryを受け取り、 statusdataを返す関数であることを除けば、上記で行ったのとほぼ同じです。 そして、それはReactアプリケーションのいくつかのコンポーネントで使用できるuseFetchフックです。

これは機能しますが、この実装の問題は、Hacker Newsに固有であるため、 useHackerNewsと呼ぶ場合があります。 私たちがやろうとしているのは、任意のURLを呼び出すために使用できるuseFetchフックを作成することです。 代わりにURLを取り込むように改良しましょう!

 const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch(url); const data = await response.json(); setData(data); setStatus('fetched'); }; fetchData(); }, [url]); return { status, data }; };

現在、useFetchフックは汎用であり、さまざまなコンポーネントで必要に応じて使用できます。

これを消費する1つの方法は次のとおりです。

 const [query, setQuery] = useState(''); const url = query && `https://hn.algolia.com/api/v1/search?query=${query}`; const { status, data } = useFetch(url);

この場合、 queryの値がtruthyの場合は、URLの設定に進みます。そうでない場合は、フックで処理されるため、undefinedを渡しても問題ありません。 エフェクトは、関係なく1回実行しようとします。

取得したデータのメモ化

メモ化は、初期段階でフェッチするための何らかの要求を行った場合に、 hackernewsエンドポイントに到達しないようにするために使用する手法です。 高価なフェッチ呼び出しの結果を保存すると、ユーザーの読み込み時間が節約されるため、全体的なパフォーマンスが向上します。

詳細については、メモ化に関するWikipediaの説明を確認してください。

それをどのように行うことができるかを探りましょう!

 const cache = {}; const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache[url]) { const data = cache[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

ここでは、URLをそれらのデータにマッピングしています。 したがって、既存のデータのフェッチをリクエストする場合は、ローカルキャッシュからデータを設定します。それ以外の場合は、リクエストを作成して結果をキャッシュに設定します。 これにより、ローカルでデータを利用できる場合にAPI呼び出しを行わないようになります。 また、URLがfalsyので、存在しないデータのフェッチに進まないようにします。 useEffectフックの前にそれを行うことはできません。これは、常にトップレベルでフックを呼び出すというフックのルールの1つに反するためです。

別のスコープでcacheを宣言することは機能しますが、フックは純粋関数の原則に反します。 さらに、コンポーネントを使用しなくなったときに、Reactが混乱をクリーンアップするのに役立つことも確認したいと思います。 useRefを調べて、それを実現するのに役立てます。

useRefを使用したデータのメモ化

useRefは、 .current propertyに可変値を保持できるボックスのようなものです。」

— ReactDocs

useRefを使用すると、変更可能な値を簡単に設定および取得でき、その値はコンポーネントのライフサイクル全体にわたって持続します。

キャッシュの実装をuseRefの魔法に置き換えましょう!

 const useFetch = (url) => { const cache = useRef({}); const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache.current[url]) { const data = cache.current[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

ここで、キャッシュはuseFetchフックにあり、初期値として空のオブジェクトがあります。

まとめ

さて、フェッチされたステータスを設定する前にデータを設定することは良い考えであると述べましたが、それには2つの潜在的な問題もあります。

  1. フェッチ状態のときにデータ配列が空になっていないため、単体テストが失敗する可能性があります。 Reactは実際には状態の変更をバッチ処理できますが、非同期でトリガーされた場合はそれを実行できません。
  2. 私たちのアプリは、必要以上に再レンダリングします。

useFetchフックの最終的なクリーンアップを行いましょう。まず、 useStateuseReducerに切り替えます。 それがどのように機能するか見てみましょう!

 const initialState = { status: 'idle', error: null, data: [], }; const [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'FETCHING': return { ...initialState, status: 'fetching' }; case 'FETCHED': return { ...initialState, status: 'fetched', data: action.payload }; case 'FETCH_ERROR': return { ...initialState, status: 'error', error: action.payload }; default: return state; } }, initialState);

ここでは、個々のuseStateのそれぞれに渡した初期値である初期状態を追加しました。 useReducerでは、実行するアクションのタイプを確認し、それに基づいて適切な値を状態に設定します。

これにより、前に説明した2つの問題が解決されます。これにより、不可能な状態や不要な再レンダリングを防ぐために、ステータスとデータを同時に設定できるようになります。

もう1つ残っているのは、副作用のクリーンアップです。 Fetchは、解決または拒否できるという意味でPromiseAPIを実装します。 一部のPromiseが解決されたために、コンポーネントがマウント解除されているときにフックが更新を行おうとすると、ReactはCan't perform a React state update on an unmounted component.

useEffectクリーンアップでそれを修正する方法を見てみましょう!

 useEffect(() => { let cancelRequest = false; if (!url) return; const fetchData = async () => { dispatch({ type: 'FETCHING' }); if (cache.current[url]) { const data = cache.current[url]; dispatch({ type: 'FETCHED', payload: data }); } else { try { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; if (cancelRequest) return; dispatch({ type: 'FETCHED', payload: data }); } catch (error) { if (cancelRequest) return; dispatch({ type: 'FETCH_ERROR', payload: error.message }); } } }; fetchData(); return function cleanup() { cancelRequest = true; }; }, [url]);

ここでは、エフェクト内でcancelRequestを定義した後、 trueに設定します。 したがって、状態を変更する前に、まずコンポーネントがアンマウントされているかどうかを確認します。 マウント解除されている場合は、状態の更新をスキップし、マウント解除されていない場合は、状態を更新します。 これにより、 React状態の更新エラーが解決され、コンポーネントの競合状態も防止されます。

結論

コンポーネントでデータをフェッチしてキャッシュするのに役立ついくつかのフックの概念を検討しました。 また、アプリ内の多くの問題を防ぐのに役立つuseEffectフックのクリーンアップも行いました。

ご不明な点がございましたら、下のコメント欄にご遠慮なくお寄せください。

  • この記事のリポジトリを参照してください→

参考文献

  • 「フックの紹介」、React Docs
  • 「ReactHooksAPIの使用を開始する」、Shedrack Akintayo
  • 「Reactフックのベストプラクティス」、Adeneye David Abiodun
  • 「関数型プログラミング:純粋関数」、Arne Brasseur