如何創建自定義 React Hook 來獲取和緩存數據

已發表: 2022-03-10
快速總結↬ React 應用程序中的許多組件很有可能必須調用 API 來檢索將顯示給用戶的數據。 使用componentDidMount()生命週期方法已經可以做到這一點,但是隨著 Hooks 的引入,您可以構建一個自定義鉤子來為您獲取和緩存數據。 這就是本教程將介紹的內容。

如果你是 React Hooks 的新手,你可以從查看官方文檔開始了解它。 之後,我建議閱讀 Shedrack Akintayo 的“React Hooks API 入門”。 為確保您繼續跟進,還有一篇由 Adeneye David Abiodun 撰寫的文章,其中涵蓋了 React Hooks 的最佳實踐,我相信這對您很有用。

在本文中,我們將使用 Hacker News Search API 來構建一個自定義的鉤子,我們可以使用它來獲取數據。 雖然本教程將介紹 Hacker News Search API,但我們將讓鉤子以某種方式工作,它將從我們傳遞給它的任何有效API 鏈接返迴響應。

最佳反應實踐

React 是一個很棒的 JavaScript 庫,用於構建豐富的用戶界面。 它提供了一個很好的組件抽象,用於將您的接口組織成功能良好的代碼,並且您可以使用它來做任何事情。 閱讀有關 React 的相關文章 →

在 React 組件中獲取數據

在 React hooks 之前,通常在 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生命週期方法在組件發生更改時被調用。 我們將狀態中的前一個查詢與當前查詢進行了比較,以防止每次我們在狀態中設置“數據”時調用該方法。 我們從使用鉤子中得到的一件事是以一種更簡潔的方式組合這兩種生命週期方法——這意味著我們不需要在組件掛載和更新時使用兩種生命週期方法。

跳躍後更多! 繼續往下看↓

使用useEffect Hook 獲取數據

一旦組件被掛載, useEffect鉤子就會被調用。 如果我們需要根據某些 prop 或狀態更改重新運行鉤子,我們需要將它們傳遞給依賴數組(這是useEffect鉤子的第二個參數)。

讓我們探索如何使用鉤子獲取數據:

 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]);

在上面的示例中,我們將query作為依賴項傳遞給我們的useEffect掛鉤。 通過這樣做,我們告訴useEffect跟踪查詢更改。 如果先前的query值與當前值不同,則再次調用useEffect

話雖如此,我們還根據需要在組件上設置了幾個status ,因為這將更好地根據一些有限狀態status向屏幕傳達一些信息。 在空閒狀態下,我們可以讓用戶知道他們可以使用搜索框開始。 在fetching狀態下,我們可以顯示一個spinner 。 而且,在獲取狀態下,我們將渲染數據。

在嘗試將狀態設置為fetched之前設置數據非常重要,這樣可以防止在設置fetched狀態時由於數據為空而發生閃爍。

創建自定義掛鉤

“自定義鉤子是一個 JavaScript 函數,其名稱以 'use' 開頭,並且可以調用其他鉤子。”

— 反應文檔

這就是它的本質,並且與 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 }; };

在這裡,我們有一個常規函數,我們接受一個可選參數,將值設置為我們的狀態,並添加可用於更新它的addsubtract方法。

在我們的應用程序中需要計數器的任何地方,我們都可以像常規函數一樣調用useCounter並傳遞一個initialState ,以便我們知道從哪裡開始計數。 當我們沒有初始狀態時,我們默認為 0。

以下是它在實踐中的工作方式:

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

我們在這裡所做的是從我們聲明它的文件中導入我們的自定義鉤子,以便我們可以在我們的應用程序中使用它。 我們將它的初始狀態設置為 100,所以每當我們調用add()時,它的count就會count 1,而每當我們調用subtract()時,它就會減少 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的函數之外,這與我們上面所做的幾乎相同。 而且,這是一個useFetch鉤子,我們可以在 React 應用程序的多個組件中使用它。

這行得通,但現在這個實現的問題是,它是 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 鉤子是通用的,我們可以在各種組件中隨意使用它。

這是消費它的一種方式:

 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,因為它會在我們的鉤子中處理。 無論如何,效果都會嘗試運行一次。

記憶獲取的數據

記憶化是一種技術,如果我們在某個初始階段發出某種請求來獲取它,我們將使用它來確保我們不會到達hackernews端點。 存儲昂貴的 fetch 調用的結果將為用戶節省一些加載時間,從而提高整體性能。

注意有關更多上下文,您可以查看 Wikipedia 對 Memoization 的解釋。

讓我們探索一下如何做到這一點!

 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鉤子之前這樣做,因為這將違反鉤子的規則之一,即始終在頂層調用鉤子。

在不同的範圍內聲明cache是可行的,但它使我們的鉤子違背了純函數的原則。 此外,當我們不再想使用該組件時,我們還希望確保 React 有助於清理我們的爛攤子。 我們將探索useRef來幫助我們實現這一目標。

使用useRef記憶數據

useRef就像一個盒子,可以在其.current property中保存一個可變值。”

— 反應文檔

使用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掛鉤中,其中一個空對像作為初始值。

包起來

好吧,我確實說過在設置獲取狀態之前設置數據是個好主意,但是我們也可能遇到兩個潛在問題:

  1. 當我們處於獲取狀態時,我們的單元測試可能會因為數據數組不為空而失敗。 React 實際上可以批處理狀態更改,但如果它是異步觸發的,它就不能這樣做;
  2. 我們的應用程序重新渲染的次數超出了應有的次數。

讓我們對我們的useFetch掛鉤進行最後的清理。我們將首先將我們的useState s 切換為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中,我們檢查我們想要執行的操作類型,並根據它設置適當的值來狀態。

這解決了我們之前討論的兩個問題,因為我們現在可以同時設置狀態和數據,以幫助防止不可能的狀態和不必要的重新渲染。

還剩下一件事:清理我們的副作用。 Fetch 實現了 Promise API,因為它可以被解決或拒絕。 如果我們的鉤子試圖在組件卸載時進行更新,因為某些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鉤子,這有助於防止我們的應用程序中出現大量問題。

如果您有任何疑問,請隨時將它們放在下面的評論部分!

  • 請參閱本文的回購 →

參考

  • “介紹 Hooks”,React 文檔
  • “React Hooks API 入門”,Shedrack Akintayo
  • “React Hooks 的最佳實踐”,Adeneye David Abiodun
  • “函數式編程:純函數”,Arne Brasseur