如何在react的Hook中異步請求數據


如何在react的Hook中請求數據

原文:How to fetch data with React Hooks?html

In this tutorial, I want to show you how to fetch data in React with Hooks by using the state and effect hooks. We will use the widely known Hacker News API to fetch popular articles from the tech world. You will also implement your custom hook for the data fetching that can be reused anywhere in your application or published on npm as standalone node package.node

本文將會講述如何在React中經過使用useState和uesEffect來fetch異步獲取數據,例子中將使用廣爲人知的Hacker News API來獲取技術領域上的一些受歡迎的文章,同時還會嘗試使用自定義Hook來獲取數據,以便在開發者的工程或者npm包中來提升複用性。react

If you don't know anything about this new React feature, checkout this introduction to React Hooks. If you want to checkout the finished project for the showcased examples that show how to fetch data in React with Hooks, checkout this GitHub repository.ios

若是你還不太清楚react的Hook新特性,那麼建議先去官方文檔或者關於hook的介紹查看相關內容,本文的所有完整示例代碼都在這個github倉庫中git

If you just want to have a ready to go React Hook for data fetching: npm install use-data-api and follow the documentation. Don't forget to star it if you use it :-)github

若是你只是想經過使用Hook來獲取數據,能夠經過npm install use-data-api,並配合文檔use-data-api文檔來使用,若是有幫助的話請➕星;shell

Note: In the future, React Hooks are not be intended for data fetching in React. Instead, a feature called Suspense will be in charge for it. The following walkthrough is nonetheless a great way to learn more about state and effect hooks in React.npm

注意:在未來,React Hooks並不計劃一直成爲異步獲取數據的方式,而是使用之後的新特性 Suspense 來獲取數據,不過經過本文的內容依然是一個學習好state和effect相關Hook的不錯方法。

DATA FETCHING WITH REACT HOOKS

If you are not familiar with data fetching in React, checkout my extensive data fetching in React article. It walks you through data fetching with React class components, how it can be made reusable with Render Prop Components and Higher-Order Components, and how it deals with error handling and loading spinners. In this article, I want to show you all of it with React Hooks in function components.redux

若是你對於react中獲取數據不太熟悉,能夠先查看相關內容進行了解,關於react獲取數據文章,文中會講述怎麼在class組件獲取數據,並經過Render props和高階組件的方式來提升複用,同時還有關於錯誤處理和加載狀態的部分,而本文將會說明在react中如何使用Hook來達到上述同樣的功能;axios

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;

The App component shows a list of items (hits = Hacker News articles). The state and state update function come from the state hook called useState that is responsible to manage the local state for the data that we are going to fetch for the App component. The initial state is an empty list of hits in an object that represents the data. No one is setting any state for this data yet.

這個App組件展現了一個列表,其中hits是指上面提到的Hacker News articles文章列表,使用useState來返回state和修改state的方法函數,咱們獲取的數據就會經過這個方法來設置到組件的state中,初始state就是一個空數組,而且沒有調用設置state的方法。

We are going to use axios to fetch data, but it is up to you to use another data fetching library or the native fetch API of the browser. If you haven't installed axios yet, you can do so by on the command line with npm install axios. Then implement your effect hook for the data fetching:

本文將使用 axios 來獲取數據,可是這能夠根據你本身的習慣來選擇其餘的庫或者直接使用fetch原生API,若是尚未安裝 axios,那麼可使用npm install axios來安裝,而後使用 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;

The effect hook called useEffect is used to fetch the data with axios from the API and to set the data in the local state of the component with the state hook's update function. The promise resolving happens with async/await.

經過使用 useEffect 來調用 axios 來獲取數據,獲取數據後經過useState返回的setState方法來修改state,state的變動會驅動DOM的更新渲染;其中使用了 async/await 來請求數據;

However, when you run your application, you should stumble into a nasty loop. The effect hook runs when the component mounts but also when the component updates. Because we are setting the state after every data fetch, the component updates and the effect runs again. It fetches the data again and again. That's a bug and needs to be avoided. We only want to fetch data when the component mounts. That's why you can provide an empty array as second argument to the effect hook to avoid activating it on component updates but only for the mounting of the component.

可是,以上代碼運行時會進入一個循環請求數據的狀態,useEffect會由於DOM的更新而不斷的執行,useEffect中求數據後設置state引發組件更新,組件更新渲染後會執行useEffect從而從新請求數據,再次從新設置state,這將會致使死循環;咱們須要避免這種狀況,咱們須要只在第一次掛載組件的時候請求就行了,useEffect方法經過第二個參數【deps 依賴】傳入一個空數組來控制,只在組件的掛載過程去執行,其餘時間不執行。

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;

The second argument can be used to define all the variables (allocated in this array) on which the hook depends. If one of the variables changes, the hook runs again. If the array with the variables is empty, the hook doesn't run when updating the component at all, because it doesn't have to watch any variables.

useEffect的第二個參數是一個包括了在當前 useEffect 中所使用的的變量的數組,若是其中某個變量變動【Object.is判斷】了,那麼在最近一次渲染後,這個useEffect會被從新執行;若是傳入的是一個空數組,那麼除了第一次掛載組件時會執行,其餘時間更新組件的時候不會執行,由於不依賴任何變量,也就是依賴不會變動,常常被用於模擬 componentDidMount,可是仍是存在區別的。

There is one last catch. In the code, we are using async/await to fetch data from a third-party API. According to the documentation every function annotated with async returns an implicit promise: "The async function declaration defines an asynchronous function, which returns an AsyncFunction object. An asynchronous function is a function which operates asynchronously via the event loop, using an implicit Promise to return its result. ". However, an effect hook should return nothing or a clean up function. That's why you may see the following warning in your developer console log: 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.. That's why using async directly in the useEffect function isn't allowed. Let's implement a workaround for it, by using the async function inside the effect.

使用空數組後還存在一個問題,咱們使用了 async/await 方式來請求接口,async/await的規範有:async 函數經過事件循環來執行異步操做,返回一個promise對象。這和react規定 useEffect 須要返回一個清除函數或者無返回值這一要求不符,因此運行上面的代碼將會在控制檯看到一段日誌提示:07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing。返回promise或者使用useEffect(async () => ...)形式是不行的,可是能夠在useEffect 內部來調用,能夠查看一下的代碼:

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;

That's data fetching with React hooks in a nutshell. But continue reading if you are interested about error handling, loading indicators, how to trigger the data fetching from a form, and how to implement a reusable data fetching hook.

交互觸發數據請求

Great, we are fetching data once the component mounts. But what about using an input field to tell the API in which topic we are interested in? "Redux" is taken as default query. But what about topics about "React"? Let's implement an input element to enable someone to fetch other stories than "Redux" stories. Therefore, introduce a new state for the input element.

咱們上面已經完成了在一個組件掛載是發起請求,不過有的時候咱們須要通過和用戶交互獲取數據來發起請求獲取數據,咱們來實踐一下經過input表單來肯定咱們須要什麼時候請求何種數據。

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

At the moment, both states are independent from each other, but now you want to couple them to only fetch articles that are specified by the query in the input field. With the following change, the component should fetch all articles by query term once it mounted.

咱們有兩個獨立的state了,此時但願可以根據input表單的內容來請求數據,經過下面的修改來將query關聯到請求中。

useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );
      setData(result.data);
    };
    fetchData();
  }, []);

One piece is missing: When you try to type something into the input field, there is no other data fetching after the mounting triggered from the effect. That's because you have provided the empty array as second argument to the effect. The effect depends on no variables, so it is only triggered when the component mounts. However, now the effect should depend on the query. Once the query changes, the data request should fire again.

還有一個問題須要調整,由於咱們手動修改input的內容時,並不會從新發起請求,這是由於上面提到的useEffect的的依賴是一個空數組,這樣在變量變動時,組件更新渲染後並不會再次調用useEffect,因此咱們須要把咱們須要的依賴 query 添加到useEffect的第二個參數依賴中,以下:

useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );
      setData(result.data);
    };
    fetchData();
  }, [query]);

The refetching of the data should work once you change the value in the input field. But that opens up another problem: On every character you type into the input field, the effect is triggered and executes another data fetching request. How about providing a button that triggers the request and therefore the hook manually?

以上是的在修改input的內容時會從新請求所需數據,可是每次的修改都會從新請求數據,最好可以經過按鈕來控制什麼時候進行數據請求;經過修改依賴,依賴於一個通過button被點擊後纔會修改的state,這樣就能夠實現想要的結果。

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('');
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );
      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>
  );
}

異步組件卸載後依然能夠setState的問題修改

在react中,一個組件中若是在異步的獲取數據過程當中,該組件已經被卸載,那麼在異步數據獲取到後setState依然會被執行,咱們能夠在hook中經過useEffect的返回清除函數來避免這種狀況;

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

若是組件卸載的話,會執行didCancel來置爲true,這樣異步請求 axios 返回的時候,設置state的時候會判斷 didCancel 來肯定當前的組件是否已經卸載,這也是js中一種巧妙的閉包的使用。

相關文章
相關標籤/搜索