データをフェッチしてキャッシュするためのカスタムReactフックを作成する方法
公開: 2022-03-10componentDidMount()
ライフサイクルメソッドを使用してこれを行うことはすでに可能ですが、フックの導入により、データをフェッチしてキャッシュするカスタムフックを構築できます。 それがこのチュートリアルでカバーするものです。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
を受け取り、 status
とdata
を返す関数であることを除けば、上記で行ったのとほぼ同じです。 そして、それは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つの潜在的な問題もあります。
- フェッチ状態のときにデータ配列が空になっていないため、単体テストが失敗する可能性があります。 Reactは実際には状態の変更をバッチ処理できますが、非同期でトリガーされた場合はそれを実行できません。
- 私たちのアプリは、必要以上に再レンダリングします。
useFetch
フックの最終的なクリーンアップを行いましょう。まず、 useState
をuseReducer
に切り替えます。 それがどのように機能するか見てみましょう!
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