(譯) 如何使用 React hooks 獲取 api 介面數據

  • 2019 年 10 月 10 日
  • 筆記

原文地址:robinwieruch 全文使用意譯,不是重要的我就沒有翻譯了

在本教程中,我想向你展示如何使用 state 和 effect 鉤子在React中獲取數據。 你還將實現自定義的 hooks 來獲取數據,可以在應用程式的任何位置重用,也可以作為獨立節點包在npm上發布。

如果你對 React 的新功能一無所知,可以查看 React hooks 的相關 api 介紹。如果你想查看完整的如何使用 React Hooks 獲取數據的項目程式碼,可以查看 github 的倉庫

如果你只是想用 React Hooks 進行數據的獲取,直接 npm i use-data-api並根據文檔進行操作。如果你使用他,別忘記給我個star 哦~

注意:將來,React Hooks 不適用於 React 中獲取數據。一個名為Suspense的功能將負責它。以下演練是了解React中有關 state 和 Effect hooks 的更多資訊的好方法。

使用 React hooks 獲取數據

如果您不熟悉React中的數據提取,請查看我在React文章中提取的大量數據。它將引導您完成使用React類組件的數據獲取,如何使用Render Prop 組件和高階組件來複用這些數據,以及它如何處理錯誤以及 loading 的。

  import React, { useState } from 'react';      function App() {      const [data, setData] = useState({ hits: [] });        return (        <ul>          {data.hits.map(item => (            <li key={item.objectID}>              <a href={item.url}>{item.title}</a>            </li>          ))}        </ul>      );    }      export default App;  

App 組件顯示了一個項目列表(hits=Hacker News 文章)。狀態和狀態更新函數來自useState 的 hook。他是來負責管理我們這個 data 的狀態的。userState 中的第一個值是data 的初始值。其實就是個解構賦值。

這裡我們使用 axios 來獲取數據,當然,你也可以使用別的開源庫。

import React, { useState, useEffect } from 'react';  import axios from 'axios';    function App() {    const [data, setData] = useState({ hits: [] });      useEffect(async () => {      const result = await axios(        'https://hn.algolia.com/api/v1/search?query=redux',      );        setData(result.data);    });      return (      <ul>        {data.hits.map(item => (          <li key={item.objectID}>            <a href={item.url}>{item.title}</a>          </li>        ))}      </ul>    );  }    export default App;  

這裡我們使用 useEffect 的 effect hook 來獲取數據。並且使用 useState 中的 setData 來更新組件狀態。

但是如上程式碼運行的時候,你會發現一個特別煩人的循環問題。effect hook 的觸發不僅僅是在組件第一次載入的時候,還有在每一次更新的時候也會觸發。由於我們在獲取到數據後就進行設置了組件狀態,然後又觸發了 effect hook。所以就會出現死循環。很顯然,這是一個 bug!我們只想在組件第一次載入的時候獲取數據 ,這也就是為什麼你可以提供一個空數組作為 useEffect 的第二個參數以避免在組件更新的時候也觸發它。當然,這樣的話,也就是在組件載入的時候觸發。

  import React, { useState, useEffect } from 'react';    import axios from 'axios';      function App() {      const [data, setData] = useState({ hits: [] });        useEffect(async () => {        const result = await axios(          'https://hn.algolia.com/api/v1/search?query=redux',        );          setData(result.data);      }, []);        return (        <ul>          {data.hits.map(item => (            <li key={item.objectID}>              <a href={item.url}>{item.title}</a>            </li>          ))}        </ul>      );    }      export default App;  

第二個參數可以用來定義 hook 所依賴的所有變數(在這個數組中),如果其中一個變數發生變化,則就會觸發這個 hook 的運行。如果傳遞的是一個空數組,則僅僅在第一次載入的時候運行。

是不是感覺 ,幹了shouldComponentUpdate 的事情

這裡還有一個陷阱。在這個程式碼裡面,我們使用 async/await 去獲取第三方的 API 的介面數據,根據文檔,每一個 async 都會返回一個 promise:async 函數聲明定義了一個非同步函數,它返回一個 AsyncFunction 對象。非同步函數是通過事件循環非同步操作的函數,使用隱式的 Promise 返回結果然而,effect hook 不應該返回任何內容,或者清除功能。這也就是為啥你看到了這個警告:07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.. ``

這就是為什麼我們不能在useEffect中使用 async的原因。但是我們可以通過如下方法解決:

  import React, { useState, useEffect } from 'react';    import axios from 'axios';      function App() {      const [data, setData] = useState({ hits: [] });        useEffect(() => {        const fetchData = async () => {          const result = await axios(            'https://hn.algolia.com/api/v1/search?query=redux',          );            setData(result.data);        };          fetchData();      }, []);        return (        <ul>          {data.hits.map(item => (            <li key={item.objectID}>              <a href={item.url}>{item.title}</a>            </li>          ))}        </ul>      );    }      export default App;  

如上就是通過 React hooks 來獲取 API 數據。但是,如果你對錯誤處理、loading、如何觸發從表單中獲取數據或者如何實現可重用的數據獲取的鉤子。請繼續閱讀。

如何自動或者手動的觸發 hook?(How to trigger a hook programmatically/manually?)

目前我們已經通過組件第一次載入的時候獲取了介面數據。但是,如何能夠通過輸入的欄位來告訴 api 介面我對那個主題感興趣呢?(就是怎麼給介面傳數據。這裡原文說的有點啰嗦(還有 redux 關鍵字來混淆視聽),我直接上程式碼吧)…

  ...      function App() {      const [data, setData] = useState({ hits: [] });      const [query, setQuery] = useState('redux');        useEffect(() => {        const fetchData = async () => {          const result = await axios(            `http://hn.algolia.com/api/v1/search?query=${query}`,          );            setData(result.data);        };          fetchData();      }, []);        return (        ...      );    }      export default App;  

這裡我跳過一段,原文實在說的太細了。

缺少一件:當你嘗試輸入欄位鍵入內容的時候,他是不會再去觸發請求的。因為你提供的是一個空數組作為useEffect的第二個參數是一個空數組,所以effect hook 的觸發不依賴任何變數,因此只在組件第一次載入的時候觸發。所以這裡我們希望當 query 這個欄位一改變的時候就觸發搜索

...    function App() {    const [data, setData] = useState({ hits: [] });    const [query, setQuery] = useState('redux');      useEffect(() => {      const fetchData = async () => {        const result = await axios(          `http://hn.algolia.com/api/v1/search?query=${query}`,        );          setData(result.data);      };        fetchData();    }, [query]);      return (      ...    );  }    export default App;  

如上,我們只是把 query作為第二個參數傳遞給了 effect hook,這樣的話,每當 query 改變的時候就會觸發搜索。但是,這樣就會出現了另一個問題:每一次的query 的欄位變動都會觸發搜索。如何提供一個按鈕來觸發請求呢?

function App() {    const [data, setData] = useState({ hits: [] });    const [query, setQuery] = useState('redux');    const [search, setSearch] = useState('redux');      useEffect(() => {      const fetchData = async () => {        const result = await axios(          `http://hn.algolia.com/api/v1/search?query=${search}`,        );          setData(result.data);      };        fetchData();    }, [search]);      return (      <Fragment>        <input          type="text"          value={query}          onChange={event => setQuery(event.target.value)}        />        <button type="button" onClick={() => setSearch(query)}>          Search        </button>          <ul>          {data.hits.map(item => (            <li key={item.objectID}>              <a href={item.url}>{item.title}</a>            </li>          ))}        </ul>      </Fragment>    );  }    

搜索的狀態設置為組件的初始化狀態,組件載入的時候就要觸發搜索,類似的查詢和搜索狀態易造成混淆,為什麼不把實際的 URL 設置為狀態而不是搜索狀態呢?

function App() {    const [data, setData] = useState({ hits: [] });    const [query, setQuery] = useState('redux');    const [url, setUrl] = useState(      'https://hn.algolia.com/api/v1/search?query=redux',    );      useEffect(() => {      const fetchData = async () => {        const result = await axios(url);          setData(result.data);      };        fetchData();    }, [url]);      return (      <Fragment>        <input          type="text"          value={query}          onChange={event => setQuery(event.target.value)}        />        <button          type="button"          onClick={() =>            setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)          }        >          Search        </button>          <ul>          {data.hits.map(item => (            <li key={item.objectID}>              <a href={item.url}>{item.title}</a>            </li>          ))}        </ul>      </Fragment>    );  }    

這是一個使用 effect hook 來獲取數據的一個例子,你可以決定 effect hook 所以依賴的狀態。一旦你點擊或者其他的什麼操作 setState 了,那麼 effect hook 就會運行。但是這個例子中,只有當你的 url 發生變化了,才會再次去獲取數據。

在 Effect Hook 中使用 Loading(Loading Indicator with React Hooks)

這裡讓我們來給程式添加一個 loading(載入器),這裡需要另一個 state

import React, { Fragment, useState, useEffect } from 'react';  import axios from 'axios';    function App() {    const [data, setData] = useState({ hits: [] });    const [query, setQuery] = useState('redux');    const [url, setUrl] = useState(      'https://hn.algolia.com/api/v1/search?query=redux',    );    const [isLoading, setIsLoading] = useState(false);      useEffect(() => {      const fetchData = async () => {        setIsLoading(true);          const result = await axios(url);          setData(result.data);        setIsLoading(false);      };        fetchData();    }, [url]);      return (      <Fragment>        <input          type="text"          value={query}          onChange={event => setQuery(event.target.value)}        />        <button          type="button"          onClick={() =>            setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)          }        >          Search        </button>          {isLoading ? (          <div>Loading ...</div>        ) : (          <ul>            {data.hits.map(item => (              <li key={item.objectID}>                <a href={item.url}>{item.title}</a>              </li>            ))}          </ul>        )}      </Fragment>    );  }    export default App;    

程式碼比較簡單,不解釋了

使用 Effect Hook 添加錯誤處理(Error Handling with React Hooks)

如何在 Effect Hook 中做一些錯誤處理呢?錯誤僅僅是一個 state ,一旦程式出現了 error state,則組件需要去渲染一些feedback 給用戶。當我們使用 async/await 的時候,我們可以使用try/catch,如下:

import React, { Fragment, useState, useEffect } from 'react';  import axios from 'axios';    function App() {    const [data, setData] = useState({ hits: [] });    const [query, setQuery] = useState('redux');    const [url, setUrl] = useState(      'https://hn.algolia.com/api/v1/search?query=redux',    );    const [isLoading, setIsLoading] = useState(false);    const [isError, setIsError] = useState(false);      useEffect(() => {      const fetchData = async () => {        setIsError(false);        setIsLoading(true);          try {          const result = await axios(url);            setData(result.data);        } catch (error) {          setIsError(true);        }          setIsLoading(false);      };        fetchData();    }, [url]);      return (      <Fragment>        <input          type="text"          value={query}          onChange={event => setQuery(event.target.value)}        />        <button          type="button"          onClick={() =>            setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)          }        >          Search        </button>          {isError && <div>Something went wrong ...</div>}          {isLoading ? (          <div>Loading ...</div>        ) : (          <ul>            {data.hits.map(item => (              <li key={item.objectID}>                <a href={item.url}>{item.title}</a>              </li>            ))}          </ul>        )}      </Fragment>    );  }    export default App;    

每一次 effect hook 運行的時候都需要重置一下 error state,這是非常有必要的。因為用戶可能想再發生錯誤的時候想再次嘗試一下。

說白了,介面給用戶回饋更加的友好

使用 React 中 Form 表單獲取數據(Fetching Data with Forms and React)

function App() {    ...      return (      <Fragment>        <form onSubmit={event => {          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);            event.preventDefault();        }}>          <input            type="text"            value={query}            onChange={event => setQuery(event.target.value)}          />          <button type="submit">Search</button>        </form>          {isError && <div>Something went wrong ...</div>}          ...      </Fragment>    );  }    

為了防止瀏覽器的 reload,我們這裡加了一個event.preventDefalut(),然後別的操作就是正常表單的操作了

自定義獲取數據的 hook(Custom Data Fetching Hook)

其實就是請求的封裝

為了能夠提取自定義的請求 hook,除了屬於輸入框的 query 欄位,別的包括 loading 載入器、錯誤處理函數都要包括在內。當然,你需要確保 App Component 所需的所有欄位在你自定義的 hook 中都有返回

const useHackerNewsApi = () => {    const [data, setData] = useState({ hits: [] });    const [url, setUrl] = useState(      'https://hn.algolia.com/api/v1/search?query=redux',    );    const [isLoading, setIsLoading] = useState(false);    const [isError, setIsError] = useState(false);      useEffect(() => {      const fetchData = async () => {        setIsError(false);        setIsLoading(true);          try {          const result = await axios(url);            setData(result.data);        } catch (error) {          setIsError(true);        }          setIsLoading(false);      };        fetchData();    }, [url]);      return [{ data, isLoading, isError }, setUrl];  }  

現在,我們可以將你的新 hook 繼續放到組件中使用

function App() {    const [query, setQuery] = useState('redux');    const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();      return (      <Fragment>        <form onSubmit={event => {          doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);            event.preventDefault();        }}>          <input            type="text"            value={query}            onChange={event => setQuery(event.target.value)}          />          <button type="submit">Search</button>        </form>          ...      </Fragment>    );  }    

通常我們需要一個初始狀態。將它簡單的傳遞給自定義 hook 中

import React, { Fragment, useState, useEffect } from 'react';  import axios from 'axios';    const useDataApi = (initialUrl, initialData) => {    const [data, setData] = useState(initialData);    const [url, setUrl] = useState(initialUrl);    const [isLoading, setIsLoading] = useState(false);    const [isError, setIsError] = useState(false);      useEffect(() => {      const fetchData = async () => {        setIsError(false);        setIsLoading(true);          try {          const result = await axios(url);            setData(result.data);        } catch (error) {          setIsError(true);        }          setIsLoading(false);      };        fetchData();    }, [url]);      return [{ data, isLoading, isError }, setUrl];  };    function App() {    const [query, setQuery] = useState('redux');    const [{ data, isLoading, isError }, doFetch] = useDataApi(      'https://hn.algolia.com/api/v1/search?query=redux',      { hits: [] },    );      return (      <Fragment>        <form          onSubmit={event => {            doFetch(              `http://hn.algolia.com/api/v1/search?query=${query}`,            );              event.preventDefault();          }}        >          <input            type="text"            value={query}            onChange={event => setQuery(event.target.value)}          />          <button type="submit">Search</button>        </form>          {isError && <div>Something went wrong ...</div>}          {isLoading ? (          <div>Loading ...</div>        ) : (          <ul>            {data.hits.map(item => (              <li key={item.objectID}>                <a href={item.url}>{item.title}</a>              </li>            ))}          </ul>        )}      </Fragment>    );  }    export default App;    

如上,就是我們使用自定義 hook 來獲取數據,該 hook 本身對 API 一無所知,它從外部接受所有的參數,但是僅管理重要的欄位,比如 data、loading、error handler 等。它執行請求並且返回組件所需要的全部數據。

用於數據獲取的 Reducer Hook(Reducer Hook for Data Fetching)

目前為止,我們使用各種 state hook 來管理數據、loading、error handler 等。然而,所有的這些狀態,通過他們自己的狀態管理,都屬於同一個整體,因為他們所關心的數據狀態都是請求相關的。正如你所看到的,他們都在 fetch 函數中使用。他們屬於同一類型的另一個很好的表現就是在函數中,他們是一個接著一個被調用的(比如:setIsError、setIsLoading)。讓我們用一個 Reducer Hook 來將這三個狀態結合起來!

一個 Reducer Hook 返回一個狀態對象和一個改變狀態對象的函數。這個函數就是 dispatch function:帶有一個 type 和參數的 action。

其實這些概念跟 redux 一毛一樣

import React, {    Fragment,    useState,    useEffect,    useReducer,  } from 'react';  import axios from 'axios';    const dataFetchReducer = (state, action) => {    ...  };    const useDataApi = (initialUrl, initialData) => {    const [url, setUrl] = useState(initialUrl);      const [state, dispatch] = useReducer(dataFetchReducer, {      isLoading: false,      isError: false,      data: initialData,    });      ...  };  

Reducer Hook將reducer函數和初始狀態對象作為參數。在我們的例子中,數據,載入和錯誤狀態的初始狀態的參數沒有改變,但它們已經聚合到一個由 reducer hook 而不是單個state hook 管理的狀態對象。

const dataFetchReducer = (state, action) => {    ...  };    const useDataApi = (initialUrl, initialData) => {    const [url, setUrl] = useState(initialUrl);      const [state, dispatch] = useReducer(dataFetchReducer, {      isLoading: false,      isError: false,      data: initialData,    });      useEffect(() => {      const fetchData = async () => {        dispatch({ type: 'FETCH_INIT' });          try {          const result = await axios(url);            dispatch({ type: 'FETCH_SUCCESS', payload: result.data });        } catch (error) {          dispatch({ type: 'FETCH_FAILURE' });        }      };        fetchData();    }, [url]);      ...  };  

現在,在獲取數據的時候,可以使用 dispathc function 來給reducer傳遞參數。使用dispatch函數發送的對象具有必需的type屬性和可選的payload屬性。該類型告訴reducer功能需要應用哪個狀態轉換,並且reducer可以另外使用有效負載來提取新狀態。畢竟,我們只有三個狀態轉換:初始化提取過程,通知成功的數據提取結果,並通知錯誤的數據提取結果。

在我們自定義的 hook 中,state 像以前一樣返回。但是因為我們有一個狀態對象而不是獨立狀態。這樣,調用useDataApi自定義鉤子的人仍然可以訪問數據,isLoading和isError:

const useDataApi = (initialUrl, initialData) => {    const [url, setUrl] = useState(initialUrl);      const [state, dispatch] = useReducer(dataFetchReducer, {      isLoading: false,      isError: false,      data: initialData,    });      ...      return [state, setUrl];  };  

最後還有我們 reducer 函數的實現。它需要作用於三個不同的狀態轉換,稱為FETCH_INIT,FETCH_SUCCESS和FETCH_FAILURE。每個狀態轉換都需要返回一個新的狀態對象。讓我們看看如何使用switch case語句實現它:

const dataFetchReducer = (state, action) => {    switch (action.type) {      case 'FETCH_INIT':        return {          ...state,          isLoading: true,          isError: false        };      case 'FETCH_SUCCESS':        return {          ...state,          isLoading: false,          isError: false,          data: action.payload,        };      case 'FETCH_FAILURE':        return {          ...state,          isLoading: false,          isError: true,        };      default:        throw new Error();    }  };  

現在,每一個 action 都有對應的處理,並且返回一個新的 state。

總之,Reducer Hook確保狀態管理的這一部分用自己的邏輯封裝。此外,你永遠不會遇到無效狀態。例如,以前可能會意外地將isLoading和isError狀態設置為true。在這種情況下,UI應該顯示什麼?現在,reducer函數定義的每個狀態轉換都會導致一個有效的狀態對象。

在 Effect Hook 中 中止數據請求(Abort Data Fetching in Effect Hook)

React中的一個常見問題是,即使組件已經卸載(例如由於使用React Router導航),也會設置組件狀態。我之前已經在這裡寫過關於這個問題的文章,它描述了如何防止在各種場景中為未載入的組件中設置狀態。讓我們看看我們如何阻止在數據提取的自定義鉤子中設置狀態:

const useDataApi = (initialUrl, initialData) => {    const [url, setUrl] = useState(initialUrl);      const [state, dispatch] = useReducer(dataFetchReducer, {      isLoading: false,      isError: false,      data: initialData,    });      useEffect(() => {      let didCancel = false;        const fetchData = async () => {        dispatch({ type: 'FETCH_INIT' });          try {          const result = await axios(url);            if (!didCancel) {            dispatch({ type: 'FETCH_SUCCESS', payload: result.data });          }        } catch (error) {          if (!didCancel) {            dispatch({ type: 'FETCH_FAILURE' });          }        }      };        fetchData();        return () => {        didCancel = true;      };    }, [url]);      return [state, setUrl];  };  

每一個 Effect Hook 都自帶一個清理功能。該功能在組件卸載時運行。清理功能是 hook 返回的一個功能。在我們的例子中,我們使用一個名為 didCancel 的 boolean 來標識組件的狀態。如果組件已卸載,則該標誌應設置為true,這將導致在最終非同步解析數據提取後阻止設置組件狀態。

注意:實際上不會中止數據獲取 - 這可以通過Axios Cancellation實現 - 但是對於 unmounted 的組件不再執行狀態轉換。由於Axios Cancellation在我看來並不是最好的API,因此這個防止設置狀態的布爾標誌也能完成這項工作。

完

Tags: