原文:https://www.robinwieruch.de/react-hooks-fetch-data

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.

在本教程中,我想向您展示如何使用State HookEfffect HookReact中获取数据。我们将使用众所周知的Hacker News API来获取科技界的热门文章。您还将为数据提取实现自定义Hook,该Hook可在应用程序中的任何位置重复使用或作为独立节点包发布在npm上。

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.

如果您对这个新的React特性一无所知,请查看此教程。如果您想在完成的项目中查看展示的示例,这些示例显示了React中如何使用Hook获取数据,请查看此GitHub存储库

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

如果你只是想有一个准备好的获取数据的React Hook:npm install use-data-api并遵循文档。如果你使用它,别忘了给它加注星标 :-)

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.

注意: 将来,React Hook不倾向于在React中获取数据。相反,一个叫做Suspense的功能将负责处理这个。下面的演练是了解更多关于在React中的使用State Hook和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.

如果你不熟悉React中的数据提取,请查看我的在React中获取数据的文章。它将引导您使用React类组件进行数据获取,如何使用Render Prop组件高阶组件使其可重用,以及如何处理错误处理和加载微调器。在本文中,我想向你展示函数组件中React Hook的所有内容。

  1. import React, { useState } from 'react';
  2. function App() {
  3. const [data, setData] = useState({ hits: [] });
  4. return (
  5. <ul>
  6. {data.hits.map(item => (
  7. <li key={item.objectID}>
  8. <a href={item.url}>{item.title}</a>
  9. </li>
  10. ))}
  11. </ul>
  12. );
  13. }
  14. 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文章)。状态(state)和状态更新函数(update function)来自名为useState的State Hook,这个Hook负责管理我们为App组件获取的数据的本地状态。初始状态是表示数据对象中的空hits列表。还没有人为此数据设置任何状态。

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来获取数据,但这取决于您使用另一个数据获取库或浏览器的本机获取API。如果尚未安装axios,则可以在命令行上使用npm install axios进行安装。然后为数据提取实现Effect Hook:

  1. import React, { useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. function App() {
  4. const [data, setData] = useState({ hits: [] });
  5. useEffect(async () => {
  6. const result = await axios(
  7. 'https://hn.algolia.com/api/v1/search?query=redux',
  8. );
  9. setData(result.data);
  10. });
  11. return (
  12. <ul>
  13. {data.hits.map(item => (
  14. <li key={item.objectID}>
  15. <a href={item.url}>{item.title}</a>
  16. </li>
  17. ))}
  18. </ul>
  19. );
  20. }
  21. 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.

名为useeffects的Effect Hook使用axios从API获取数据,并使用State Hook的更新函数将数据设置为组件的本地状态。The promise resolving happens with 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.

然而,当运行你的应用程序时,你应该偶然发现一个讨厌的循环。Effect Hook在组件安装时运行,但在组件更新时也会运行。因为我们在每次数据提取后都设置状态,所以组件会更新,Effect会再次运行。它一次又一次地获取数据。这是一个错误,需要避免。我们只想在组件挂载时获取数据。这就是你提供一个空数组作为Effect Hook的第二个参数的原因,这这样做可以避免在组件更新时激活它,仅用于组件装载的时候做一次。

  1. import React, { useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. function App() {
  4. const [data, setData] = useState({ hits: [] });
  5. useEffect(async () => {
  6. const result = await axios(
  7. 'https://hn.algolia.com/api/v1/search?query=redux',
  8. );
  9. setData(result.data);
  10. }, []);
  11. return (
  12. <ul>
  13. {data.hits.map(item => (
  14. <li key={item.objectID}>
  15. <a href={item.url}>{item.title}</a>
  16. </li>
  17. ))}
  18. </ul>
  19. );
  20. }
  21. 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.

第二个参数可用于定义Hook所依赖的所有变量 (在此数组中分配)。如果其中一个变量发生变化,Hook将再次运行。如果带有变量的数组为空,则在更新组件时根本不会运行Hook,因为它不必监视任何变量。

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/wait从第三方API获取数据。根据文档,每个带有async注释的函数都会返回一个隐式承诺(Proise): “async函数声明定义了一个异步函数,该函数返回一个AsyncFunction对象。异步函数是通过事件循环异步运行的函数,使用隐式承诺返回其结果。”。但是,Effect Hook将不返回任何内容或清理函数。这就是为什么您可能会在开发人员控制台日志中看到以下警告: 07:41:22.910 index.js:1452警告: useEffects函数必须返回清理函数或不返回任何内容。不支持Promises 和 useEffect (async () => …),但是您可以在Effect中调用异步函数。这就是为什么不允许在useEffect函数中直接使用async的原因。让我们通过在Effect中使用异步函数来实现它的解决方法。

  1. import React, { useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. function App() {
  4. const [data, setData] = useState({ hits: [] });
  5. useEffect(() => {
  6. const fetchData = async () => {
  7. const result = await axios(
  8. 'https://hn.algolia.com/api/v1/search?query=redux',
  9. );
  10. setData(result.data);
  11. };
  12. fetchData();
  13. }, []);
  14. return (
  15. <ul>
  16. {data.hits.map(item => (
  17. <li key={item.objectID}>
  18. <a href={item.url}>{item.title}</a>
  19. </li>
  20. ))}
  21. </ul>
  22. );
  23. }
  24. 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.

简而言之,这就是用React Hook获取数据。但是,如果你对错误处理、加载指示、如何触发从表单获取数据以及如何实现可重用的数据获取hook感兴趣,请继续阅读。

HOW TO TRIGGER A HOOK PROGRAMMATICALLY / MANUALLY?

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.

太好了,一旦组件安装,我们就会获取数据。但是,如何使用输入字段告诉API我们对哪个主题感兴趣?将 “Redux” 作为默认查询。但是关于 “React” 的话题呢?让我们实现一个输入元素,使某人能够获取 “Redux” 故事以外的其他故事。因此,为输入元素引入新的状态。

  1. import React, { Fragment, useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. function App() {
  4. const [data, setData] = useState({ hits: [] });
  5. const [query, setQuery] = useState('redux');
  6. useEffect(() => {
  7. const fetchData = async () => {
  8. const result = await axios(
  9. 'https://hn.algolia.com/api/v1/search?query=redux',
  10. );
  11. setData(result.data);
  12. };
  13. fetchData();
  14. }, []);
  15. return (
  16. <Fragment>
  17. <input
  18. type="text"
  19. value={query}
  20. onChange={event => setQuery(event.target.value)}
  21. />
  22. <ul>
  23. {data.hits.map(item => (
  24. <li key={item.objectID}>
  25. <a href={item.url}>{item.title}</a>
  26. </li>
  27. ))}
  28. </ul>
  29. </Fragment>
  30. );
  31. }
  32. 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.

目前,两个状态彼此不相关,但是现在您希望将它们配对以仅获取由输入字段中的查询指定的文章。通过以下更改,组件在安装后应通过查询术语获取所有文章。

  1. ...
  2. function App() {
  3. const [data, setData] = useState({ hits: [] });
  4. const [query, setQuery] = useState('redux');
  5. useEffect(() => {
  6. const fetchData = async () => {
  7. const result = await axios(
  8. `http://hn.algolia.com/api/v1/search?query=${query}`,
  9. );
  10. setData(result.data);
  11. };
  12. fetchData();
  13. }, []);
  14. return (
  15. ...
  16. );
  17. }
  18. export default App;


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.

缺少一个部分: 当您尝试在输入字段中键入内容时,在从effect触发安装后,没有其他数据获取。这是因为您提供了空数组作为效果的第二个参数。Effect依赖变量,因此仅在组件挂载时触发。但是,现在Effect应该取决于查询。一旦查询更改,数据请求应再次触发。

  1. ...
  2. function App() {
  3. const [data, setData] = useState({ hits: [] });
  4. const [query, setQuery] = useState('redux');
  5. useEffect(() => {
  6. const fetchData = async () => {
  7. const result = await axios(
  8. `http://hn.algolia.com/api/v1/search?query=${query}`,
  9. );
  10. setData(result.data);
  11. };
  12. fetchData();
  13. }, [query]);
  14. return (
  15. ...
  16. );
  17. }
  18. export default App;

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?

更改输入字段中的值后就会获取数据。但这带来了另一个问题: 在输入字段中键入的每个字符上,都会触发效果并执行另一个数据提取请求。提供一个手动触发请求并因此触发Hook的按钮怎么样?

  1. function App() {
  2. const [data, setData] = useState({ hits: [] });
  3. const [query, setQuery] = useState('redux');
  4. const [search, setSearch] = useState('');
  5. useEffect(() => {
  6. const fetchData = async () => {
  7. const result = await axios(
  8. `http://hn.algolia.com/api/v1/search?query=${query}`,
  9. );
  10. setData(result.data);
  11. };
  12. fetchData();
  13. }, [query]);
  14. return (
  15. <Fragment>
  16. <input
  17. type="text"
  18. value={query}
  19. onChange={event => setQuery(event.target.value)}
  20. />
  21. <button type="button" onClick={() => setSearch(query)}>
  22. Search
  23. </button>
  24. <ul>
  25. {data.hits.map(item => (
  26. <li key={item.objectID}>
  27. <a href={item.url}>{item.title}</a>
  28. </li>
  29. ))}
  30. </ul>
  31. </Fragment>
  32. );
  33. }

Now, make the effect dependant on the search state rather than the fluctuant query state that changes with every key stroke in the input field. Once the user clicks the button, the new search state is set and should trigger the effect hook kinda manually.

现在,Effect依赖于搜索状态,而不是随输入字段中每次按键而变化的波动查询状态。用户单击按钮后,将设置新的搜索状态,并应手动触发Effect Hook。

  1. ...
  2. function App() {
  3. const [data, setData] = useState({ hits: [] });
  4. const [query, setQuery] = useState('redux');
  5. const [search, setSearch] = useState('redux');
  6. useEffect(() => {
  7. const fetchData = async () => {
  8. const result = await axios(
  9. `http://hn.algolia.com/api/v1/search?query=${search}`,
  10. );
  11. setData(result.data);
  12. };
  13. fetchData();
  14. }, [search]);
  15. return (
  16. ...
  17. );
  18. }
  19. export default App;


Also the initial state of the search state is set to the same state as the query state, because the component fetches data also on mount and therefore the result should mirror the value in the input field. However, having a similar query and search state is kinda confusing. Why not set the actual URL as state instead of the search state?

此外,搜索状态的初始值被设置为与查询状态相同,因为组件也在装载时获取数据,所以结果应该镜像输入字段的值。然而,具有相同的查询和搜索状态有点令人困惑。为什么不把state设置为实际的URL而不是搜索state呢?

  1. function App() {
  2. const [data, setData] = useState({ hits: [] });
  3. const [query, setQuery] = useState('redux');
  4. const [url, setUrl] = useState(
  5. 'https://hn.algolia.com/api/v1/search?query=redux',
  6. );
  7. useEffect(() => {
  8. const fetchData = async () => {
  9. const result = await axios(url);
  10. setData(result.data);
  11. };
  12. fetchData();
  13. }, [url]);
  14. return (
  15. <Fragment>
  16. <input
  17. type="text"
  18. value={query}
  19. onChange={event => setQuery(event.target.value)}
  20. />
  21. <button
  22. type="button"
  23. onClick={() =>
  24. setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
  25. }
  26. >
  27. Search
  28. </button>
  29. <ul>
  30. {data.hits.map(item => (
  31. <li key={item.objectID}>
  32. <a href={item.url}>{item.title}</a>
  33. </li>
  34. ))}
  35. </ul>
  36. </Fragment>
  37. );
  38. }

That’s if for the implicit programmatic data fetching with the effect hook. You can decide on which state the effect depends. Once you set this state on a click or in another side-effect, this effect will run again. In this case, if the URL state changes, the effect runs again to fetch stories from the API.

这就是使用Effect Hook获取隐式编程数据的方法。你可以决定Effect依赖于哪个State。在单击或其他side-effect(副作用)中设置此状态后,Effect将再次运行。在这种情况下,如果URL State变化了,则Effect将再次运行以从API获取数据。

LOADING INDICATOR WITH REACT HOOKS

Let’s introduce a loading indicator to the data fetching. It’s just another state that is manage by a state hook. The loading flag is used to render a loading indicator in the App component.

我们来给拉取数据过程引入一个加载指示器。这只是由Sate Hook管理的另一个State。加载标志用于在应用程序组件中呈现加载指示器。

  1. import React, { Fragment, useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. function App() {
  4. const [data, setData] = useState({ hits: [] });
  5. const [query, setQuery] = useState('redux');
  6. const [url, setUrl] = useState(
  7. 'https://hn.algolia.com/api/v1/search?query=redux',
  8. );
  9. const [isLoading, setIsLoading] = useState(false);
  10. useEffect(() => {
  11. const fetchData = async () => {
  12. setIsLoading(true);
  13. const result = await axios(url);
  14. setData(result.data);
  15. setIsLoading(false);
  16. };
  17. fetchData();
  18. }, [url]);
  19. return (
  20. <Fragment>
  21. <input
  22. type="text"
  23. value={query}
  24. onChange={event => setQuery(event.target.value)}
  25. />
  26. <button
  27. type="button"
  28. onClick={() =>
  29. setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
  30. }
  31. >
  32. Search
  33. </button>
  34. {isLoading ? (
  35. <div>Loading ...</div>
  36. ) : (
  37. <ul>
  38. {data.hits.map(item => (
  39. <li key={item.objectID}>
  40. <a href={item.url}>{item.title}</a>
  41. </li>
  42. ))}
  43. </ul>
  44. )}
  45. </Fragment>
  46. );
  47. }
  48. export default App;


Once the effect is called for data fetching, which happens when the component mounts or the URL state changes, the loading state is set to true. Once the request resolves, the loading state is set to false again.

当组件挂载或URL状态更改时,加载状态将设置为true,然后调用Effect获取数据。一旦请求完成,加载状态将再次设置为false。

ERROR HANDLING WITH REACT HOOKS

What about error handling for data fetching with a React hook? The error is just another state initialized with a state hook. Once there is an error state, the App component can render feedback for the user. When using async/await, it is common to use try/catch blocks for error handling. You can do it within the effect:

使用React Hook获取数据的错误处理如何?错误只是使用State Hook初始化的另一个状态。一旦出现错误状态,应用程序组件就可以为用户提供反馈。使用async/wait时,通常使用try/catch块进行错误处理。您可以在Effect范围内完成:

  1. import React, { Fragment, useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. function App() {
  4. const [data, setData] = useState({ hits: [] });
  5. const [query, setQuery] = useState('redux');
  6. const [url, setUrl] = useState(
  7. 'https://hn.algolia.com/api/v1/search?query=redux',
  8. );
  9. const [isLoading, setIsLoading] = useState(false);
  10. const [isError, setIsError] = useState(false);
  11. useEffect(() => {
  12. const fetchData = async () => {
  13. setIsError(false);
  14. setIsLoading(true);
  15. try {
  16. const result = await axios(url);
  17. setData(result.data);
  18. } catch (error) {
  19. setIsError(true);
  20. }
  21. setIsLoading(false);
  22. };
  23. fetchData();
  24. }, [url]);
  25. return (
  26. <Fragment>
  27. <input
  28. type="text"
  29. value={query}
  30. onChange={event => setQuery(event.target.value)}
  31. />
  32. <button
  33. type="button"
  34. onClick={() =>
  35. setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
  36. }
  37. >
  38. Search
  39. </button>
  40. {isError && <div>Something went wrong ...</div>}
  41. {isLoading ? (
  42. <div>Loading ...</div>
  43. ) : (
  44. <ul>
  45. {data.hits.map(item => (
  46. <li key={item.objectID}>
  47. <a href={item.url}>{item.title}</a>
  48. </li>
  49. ))}
  50. </ul>
  51. )}
  52. </Fragment>
  53. );
  54. }
  55. export default App;


The error state is reset every time the hook runs again. That’s useful because after a failed request the user may want to try it again which should reset the error. In order to enforce an error yourself, you can alter the URL into something invalid. Then check whether the error message shows up.

每当再次运行Hook时,都会重置错误状态。这很有用,因为在请求失败后,用户可能想再试一次,这应该重置错误。为了自己强制执行错误,您可以将URL更改为无效的内容。然后检查是否显示错误消息。

FETCHING DATA WITH FORMS AND REACT

What about a proper form to fetch data? So far, we have only a combination of input field and button. Once you introduce more input elements, you may want to wrap them with a form element. In addition, a form makes it possible to trigger the button with “Enter” on the keyboard too.

怎么样用一个通常的表单来获取数据呢?到目前为止,我们只有输入字段和按钮的组合。引入更多输入元素后,可能需要使用表单元素包装它们。而且,使用表单使得可以用 “Enter” 键触发按钮。

  1. function App() {
  2. ...
  3. return (
  4. <Fragment>
  5. <form
  6. onSubmit={() =>
  7. setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
  8. }
  9. >
  10. <input
  11. type="text"
  12. value={query}
  13. onChange={event => setQuery(event.target.value)}
  14. />
  15. <button type="submit">Search</button>
  16. </form>
  17. {isError && <div>Something went wrong ...</div>}
  18. ...
  19. </Fragment>
  20. );
  21. }


But now the browser reloads when clicking the submit button, because that’s the native behavior of the browser when submitting a form. In order to prevent the default behavior, we can invoke a function on the React event. That’s how you do it in React class components too.

但是现在,浏览器在单击 “提交” 按钮时会重新加载,因为这是浏览器在提交表单时的原始行为。为了防止默认行为,我们可以在React事件上调用函数。这也是教你如何在React类组件中做到这一点。

  1. function App() {
  2. ...
  3. return (
  4. <Fragment>
  5. <form onSubmit={(event) => {
  6. setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
  7. event.preventDefault();
  8. }}>
  9. <input
  10. type="text"
  11. value={query}
  12. onChange={event => setQuery(event.target.value)}
  13. />
  14. <button type="submit">Search</button>
  15. </form>
  16. {isError && <div>Something went wrong ...</div>}
  17. ...
  18. </Fragment>
  19. );
  20. }


Now the browser shouldn’t reload anymore when you click the submit button. It works as before, but this time with a form instead of the naive input field and button combination. You can press the “Enter” key on your keyboard too.

现在,当你单击 “提交” 按钮时,浏览器不应再重新加载。它像以前一样工作,但是这次使用的是表单,而不是朴素的输入字段和按钮组合。你也可以按键盘上的 “回车键”。

CUSTOM DATA FETCHING HOOK

In order to extract a custom hook for data fetching, move everything that belongs to the data fetching, except for the query state that belongs to the input field, but including the loading indicator and error handling, to its own function. Also make sure you return all the necessary variables from the function that are used in the App component.

为了提取用于数据获取的自定义Hook,请把输入字段的查询状态以外的那些提取数据的代码(包括加载指示器和错误处理)移动到他自己的函数内。另外,请确保从这个新函数中返回App组件使用的所有必要变量。

  1. const useHackerNewsApi = () => {
  2. const [data, setData] = useState({ hits: [] });
  3. const [url, setUrl] = useState(
  4. 'https://hn.algolia.com/api/v1/search?query=redux',
  5. );
  6. const [isLoading, setIsLoading] = useState(false);
  7. const [isError, setIsError] = useState(false);
  8. useEffect(() => {
  9. const fetchData = async () => {
  10. setIsError(false);
  11. setIsLoading(true);
  12. try {
  13. const result = await axios(url);
  14. setData(result.data);
  15. } catch (error) {
  16. setIsError(true);
  17. }
  18. setIsLoading(false);
  19. };
  20. fetchData();
  21. }, [url]);
  22. return [{ data, isLoading, isError }, setUrl];
  23. }

Now, your new hook can be used in the App component again:

现在,你可以在App组件中使用新的Hook:

  1. function App() {
  2. const [query, setQuery] = useState('redux');
  3. const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();
  4. return (
  5. <Fragment>
  6. <form onSubmit={(event) => {
  7. doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);
  8. event.preventDefault();
  9. }}>
  10. <input
  11. type="text"
  12. value={query}
  13. onChange={event => setQuery(event.target.value)}
  14. />
  15. <button type="submit">Search</button>
  16. </form>
  17. ...
  18. </Fragment>
  19. );
  20. }


The initial state can be made generic too. Pass it simply to the new custom hook:

初始状态也可以变得通用。只需将其传递给新的自定义Hook:

  1. import React, { Fragment, useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. const useDataApi = (initialUrl, initialData) => {
  4. const [data, setData] = useState(initialData);
  5. const [url, setUrl] = useState(initialUrl);
  6. const [isLoading, setIsLoading] = useState(false);
  7. const [isError, setIsError] = useState(false);
  8. useEffect(() => {
  9. const fetchData = async () => {
  10. setIsError(false);
  11. setIsLoading(true);
  12. try {
  13. const result = await axios(url);
  14. setData(result.data);
  15. } catch (error) {
  16. setIsError(true);
  17. }
  18. setIsLoading(false);
  19. };
  20. fetchData();
  21. }, [url]);
  22. return [{ data, isLoading, isError }, setUrl];
  23. };
  24. function App() {
  25. const [query, setQuery] = useState('redux');
  26. const [{ data, isLoading, isError }, doFetch] = useDataApi(
  27. 'https://hn.algolia.com/api/v1/search?query=redux',
  28. { hits: [] },
  29. );
  30. return (
  31. <Fragment>
  32. <form
  33. onSubmit={(event) => {
  34. doFetch(
  35. `http://hn.algolia.com/api/v1/search?query=${query}`,
  36. );
  37. event.preventDefault();
  38. }}
  39. >
  40. <input
  41. type="text"
  42. value={query}
  43. onChange={event => setQuery(event.target.value)}
  44. />
  45. <button type="submit">Search</button>
  46. </form>
  47. {isError && <div>Something went wrong ...</div>}
  48. {isLoading ? (
  49. <div>Loading ...</div>
  50. ) : (
  51. <ul>
  52. {data.hits.map(item => (
  53. <li key={item.objectID}>
  54. <a href={item.url}>{item.title}</a>
  55. </li>
  56. ))}
  57. </ul>
  58. )}
  59. </Fragment>
  60. );
  61. }
  62. export default App;


That’s it for the data fetching with a custom hook. The hook itself doesn’t know anything about the API. It receives all parameters from the outside and only manages necessary states such as the data, loading and error state. It executes the request and returns the data to the component using it as custom data fetching hook.

这就是使用自定义Hook获取数据的方法。Hook自己并不了解对API的内容,它从外部接收所有参数,并且仅管理必要的状态,例如数据、加载和错误状态。它执行请求并将数据作为自定义数据提取Hook返回到组件。

REDUCER HOOK FOR DATA FETCHING

So far, we have used various state hooks to manage our data fetching state for the data, loading and error state. However, somehow all these states, managed with their own state hook, belong together because they care about the same cause. As you can see, they are all used within the data fetching function. A good indicator that they belong together is that they are used one after another (e.g. setIsError, setIsLoading). Let’s combine all three of them with a Reducer Hook instead.

到目前为止,我们已经使用各种状态Hook来管理数据获取状态、加载状态和错误状态。然而,因为某种原因,所有这些状态都是用他们各自的State Hook来管理,因为他们关注对象响应而被放在一起。如你所见,它们都在数据提取函数中使用。它们属于一起的一个很好的代表是它们被一个接一个地使用 (例如setIsErrorsetIsLoading)。现在让我们把它们三个都用Reducer Hook组合起来。

A Reducer Hook returns us a state object and a function to alter the state object. The function — called dispatch function — takes an action which has a type and an optional payload. All this information is used in the actual reducer function to distill a new state from the previous state, the action’s optional payload and type. Let’s see how this works in code:

Reducer Hook返回给我们一个状态对象和一个改变状态对象的函数。这个函数 称为dispatch函数,他带着类型(type)和可选的payload(作为参数)来行动。所有这些信息都在实际的Reducer函数中使用,从以前的状态 、type和可选的payload中提取新状态。让我们看看这在代码中是如何工作的:

  1. import React, {
  2. Fragment,
  3. useState,
  4. useEffect,
  5. useReducer,
  6. } from 'react';
  7. import axios from 'axios';
  8. const dataFetchReducer = (state, action) => {
  9. ...
  10. };
  11. const useDataApi = (initialUrl, initialData) => {
  12. const [url, setUrl] = useState(initialUrl);
  13. const [state, dispatch] = useReducer(dataFetchReducer, {
  14. isLoading: false,
  15. isError: false,
  16. data: initialData,
  17. });
  18. ...
  19. };

The Reducer Hook takes the reducer function and an initial state object as parameters. In our case, the arguments of the initial states for the data, loading and error state didn’t change, but they have been aggregated to one state object managed by one reducer hook instead of single state hooks.

Reducer Hook将reducer函数和初始状态对象作为参数。在我们的案例中,数据状态、加载状态和错误状态的初始参数没有改变,但是它们已经聚合到一个状态对象,由一个Reducer Hook而不是单个State Hook管理。

  1. const dataFetchReducer = (state, action) => {
  2. ...
  3. };
  4. const useDataApi = (initialUrl, initialData) => {
  5. const [url, setUrl] = useState(initialUrl);
  6. const [state, dispatch] = useReducer(dataFetchReducer, {
  7. isLoading: false,
  8. isError: false,
  9. data: initialData,
  10. });
  11. useEffect(() => {
  12. const fetchData = async () => {
  13. dispatch({ type: 'FETCH_INIT' });
  14. try {
  15. const result = await axios(url);
  16. dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
  17. } catch (error) {
  18. dispatch({ type: 'FETCH_FAILURE' });
  19. }
  20. };
  21. fetchData();
  22. }, [url]);
  23. ...
  24. };


Now, when fetching data, the dispatch function can be used to send information to the reducer function. The object being send with the dispatch function has a mandatory type property and an optional payload property. The type tells the reducer function which state transition needs to be applied and the payload can additionally be used by the reducer to distill the new state. After all, we only have three state transitions: initializing the fetching process, notifying about a successful data fetching result, and notifying about an errornous data fetching result.

现在,在获取数据时,调度函数(dispatch function)可用于向reducer函数发送信息。使用dispatch函数发送的对象具有强制type属性和可选payload属性。type告诉reducer函数需要应用哪个状态转换,并且reducer还可以使用payload来提取新状态。毕竟,我们只有三个状态转换: 初始化提取过程,通知成功的数据提取结果,以及通知错误的数据提取结果。

In the end of the custom hook, the state is returned as before, but because we have a state object and not the standalone states anymore. This way, the one who calls the useDataApicustom hook still gets access to data, isLoading and isError:

在自定义Hook的末尾,状态像以前一样返回,但是因为我们有一个状态对象,而不是独立状态。这样调用useDataApi 自定义hook的人仍然可以访问dataisLoadingisError:

  1. const useDataApi = (initialUrl, initialData) => {
  2. const [url, setUrl] = useState(initialUrl);
  3. const [state, dispatch] = useReducer(dataFetchReducer, {
  4. isLoading: false,
  5. isError: false,
  6. data: initialData,
  7. });
  8. ...
  9. return [state, setUrl];
  10. };

Last but not least, the implementation of the reducer function is missing. It needs to act on three different state transitions called FETCH_INIT, FETCH_SUCCESS and FETCH_FAILURE. Each state transition needs to return a new state object. Let’s see how this can be implemented with a switch case statement:

最后(但并非最不重要)的一点是,还没有实现reducer函数。它需要作用于三种不同的状态转换,称为FETCH_INITFETCH_SUCCESSFETCH_FAILURE。每个状态转换都需要返回一个新的状态对象。让我们看看如何使用switch case语句来实现这一点:

  1. const dataFetchReducer = (state, action) => {
  2. switch (action.type) {
  3. case 'FETCH_INIT':
  4. return { ...state };
  5. case 'FETCH_SUCCESS':
  6. return { ...state };
  7. case 'FETCH_FAILURE':
  8. return { ...state };
  9. default:
  10. throw new Error();
  11. }
  12. };

A reducer function has access to the current state and the incoming action via its arguments. So far, in out switch case statement each state transition only returns the previous state. A destructuring statement is used to keep the state object immutable — meaning the state is never directly mutated — to enforce best practices. Now let’s override a few of the current’s state returned properties to alter the state with each state transition:

reducer函数可以通过其参数访问当前状态和传入操作。到目前为止,switch case语句每个状态转换仅返回先前的状态。分解语句用于保持状态对象不可变 (意味着状态永远不会直接突变),以实施最佳实践。现在,让我们覆盖一些当前状态返回的属性,以便在每个状态转换时更改状态:

  1. const dataFetchReducer = (state, action) => {
  2. switch (action.type) {
  3. case 'FETCH_INIT':
  4. return {
  5. ...state,
  6. isLoading: true,
  7. isError: false
  8. };
  9. case 'FETCH_SUCCESS':
  10. return {
  11. ...state,
  12. isLoading: false,
  13. isError: false,
  14. data: action.payload,
  15. };
  16. case 'FETCH_FAILURE':
  17. return {
  18. ...state,
  19. isLoading: false,
  20. isError: true,
  21. };
  22. default:
  23. throw new Error();
  24. }
  25. };

Now every state transition, decided by the action’s type, returns a new state based on the previous state and the optional payload. For instance, in the case of a successful request, the payload is used to set the data of the new state object.

现在,由操作类型决定的每个状态转换都会根据先前的状态和可选的payload返回一个新状态。例如,在请求成功的情况下,payload用于设置新状态对象的数据。

In conclusion, the Reducer Hook makes sure that this portion of the state management is encapsulated with its own logic. By providing action types and optional payloads, you will always end up with a predicatbale state change. In addition, you will never run into invalid states. For instance, previously it would have been possible to accidently set the isLoading and isErrorstates to true. What should be displayed in the UI for this case? Now, each state transition defined by the reducer function leads to a valid state object.
总之,Reduer Hook确保状态管理的这一部分用自己的逻辑封装。通过提供操作类型和可选payload,您将始终以可预测的状态更改结束。此外,您将永远不会遇到无效状态。例如,以前可能会意外地将isLoadingisError状态设置为true。在这种情况下,用户界面中应该显示什么?现在,由reducer函数定义的每个状态转换都会导致有效的状态对象。

ABORT DATA FETCHING IN EFFECT HOOK

It’s a common problem in React that component state is set even though the component got already unmounted (e.g. due to navigating away with React Router). I have written about this issue previously over here which describes how to prevent setting state for unmounted components in various scenarios. Let’s see how we can prevent to set state in our custom hook for the data fetching:

即使组件已经卸载 (例如,由于使用React路由导航离开),在React中设置组件状态也是一个常见问题。我之前在这里写过这个问题,它描述了如何在各种情况下防止未安装组件的设置状态。让我们看看如何防止在数据提取自定义Hook中置状态:

  1. const useDataApi = (initialUrl, initialData) => {
  2. const [url, setUrl] = useState(initialUrl);
  3. const [state, dispatch] = useReducer(dataFetchReducer, {
  4. isLoading: false,
  5. isError: false,
  6. data: initialData,
  7. });
  8. useEffect(() => {
  9. let didCancel = false;
  10. const fetchData = async () => {
  11. dispatch({ type: 'FETCH_INIT' });
  12. try {
  13. const result = await axios(url);
  14. if (!didCancel) {
  15. dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
  16. }
  17. } catch (error) {
  18. if (!didCancel) {
  19. dispatch({ type: 'FETCH_FAILURE' });
  20. }
  21. }
  22. };
  23. fetchData();
  24. return () => {
  25. didCancel = true;
  26. };
  27. }, [url]);
  28. return [state, setUrl];
  29. };

Every Effect Hook comes with a clean up function which runs when a component unmounts. The clean up function is the one function returned from the hook. In our case, we use a boolean flag called didCancel to let our data fetching logic know about the state (mounted/unmounted) of the component. If the component did unmount, the flag should be set to true which results in preventing to set the component state after the data fetching has been asynchronously resolved eventually.

每个Effect Hook都有一个清理功能,当组件卸载时运行。清理函数是从Hook返回的一个函数。在我们的例子中,我们使用一个名为didCancel的布尔标志来让我们的数据获取逻辑知道组件的状态 (mounted / unmounted)。如果组件确实卸载了,则标志应设置为true,这将导致在最终异步解析数据提取后阻止设置组件状态。

Note: Actually not the data fetching is aborted — which could be achieved with Axios Cancellation — but the state transition is not performed anymore for the unmounted component. Since Axios Cancellation has not the best API in my eyes, this boolean flag to prevent setting state does the job as well.

注意: 实际上并没有中止数据提取 — 这可以通过Axios Cancellation 来实现 — 但是不再对未安装的组件执行状态转换。由于 Axios Cancellation在我看来不是最好的API,这个防止设置状态的布尔标志也可以完成工作。


You have learned how the React hooks for state and effects can be used in React for data fetching. If you are curious about data fetching in class components (and function components) with render props and higher-order components, checkout out my other article from the beginning. Otherwise, I hope this article was useful to you for learning about React Hooks and how to use them in a real world scenario.

您已经了解了如何在获取数据时使用state和effect的React Hook。如果您对使用渲染props和高阶组件在类组件 (和函数组件) 中获取数据感到好奇,请从头开始查看我的其他文章。否则,我希望这篇文章对你学习Effect Hook以及如何在现实世界中使用它们是有用的。