前言

在学习 React Hooks 的时候发现了这一篇博客。由于原文是英文版,本着学习与分享的目的,翻译了这一篇博客。第一次翻译,翻译的很烂。

阅读指南

这篇博客将会手把手的教你使用 State 和 Effect hooks 获取数据。在学习之前请确保你对 React 以及 React Hooks 有一定的了解(知道大概是什么东西就行)。

你能收获什么?

  • 了解 React 如何使用 Hacker News API 获取数据
  • States Hook 和 Effect Hook 的基本使用
  • Reducer Hook 的基本使用
  • 自定义一个可复用的用于获取数据的 Hook

如果你并不清楚 React 的新特性,那么你可以查阅 React Hooks 入门。如果你想查看已完成的项目中使用 React Hooks 获取数据的例子,请查看这个 GitHub repository

如果你只是想要一个能够即开即用的用于获取数据的 React Hook:

如果你使用了它不要忘记给一个 star ~

使用 React Hooks 获取数据

  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;

App组件将会展示一组列表,列表中的的

这个 APP 组件将会展示一组 items ( hits = Hacker News articles)。状态以及状态更新的函数来自于一个叫做 useState 的状态钩子,这个钩子将会负责管理我们在 APP Component 中获取的数据在本地的状态[1]。初始的状态是一个代表着数据的对象,其中的 hits 属性初试为一个空数组。现在还没有人给这个数据设置任何的状态。

接下来我打算使用 axios 获取数据,不过你自己也可以决定使用其它的库或者是浏览器原生的 fetch 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;

被称为useEffect的 effect hook 被用来从 API 中用 axios 获取数据,并通过 state hook’s 的更新函数在组件的本地状态中设置数据。并且使用 async/await 的写法来实现 Promise 中的 resolving 状态。

然而,当你运行应用的时候,你可能会被一个讨厌的循环所难倒[2]。effect hook 不仅仅会在组件挂载的时候执行,还会在组件更新的时候执行。因为我们在每一次获取数据之后都会设置状态,所以当组件更新的时候,effect 将会再次执行。这便导致了组件一遍又一遍的获取数据。这是一个 bug ,我们必须要去避免它。我们只想让组件挂载的时候获取数据。 这就是为什么你需要提供一个空的数组作为 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;

第二个参数被用来定义 hook 所依赖的所有变量(分配在这个数组中)。如果其中一个变量改变了,那么 hook将会重新运行一次。如果这个数组是一个空数组,那么当组件更新的时候,hook 不会运行,因为它并没有监视(watch)任何变量。

还有最后一个问题。在上面的代码中,我们使用 async/await 从第三方的 API 获取数据。根据文档,每一个使用了 async 注解的函数都会返回一个隐式的 promise:“async 函数声明定义了一个异步的函数,这个函数将会返回一个 AsyncFunction object。异步函数是一个通过 event loop 来实现异步操作的函数,并且将一个隐式的 Promise 作为函数返回的结果” 。然而,effect hook 需要我们返回 nothing 或者返回一个清理函数。这就是为什么你可能会在开发者控制台中看见如下的警告:

  1. 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 的原因。让我们在 effect 中使用另一种方法来使用 async。

  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;

这就是使用使用 React hooks 获取数据的简述。但如果你对于处理错误,加载 indicators,如何从表单中触发数据的获取以及如何实现一个可复用的 hook 感兴趣,那么你可以继续读下去。

注解:

  1. 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.
  2. 你可以通过Chrome控制台中的网络面板查看网络请求来观察这个讨厌的循环

如何使用编程/手动的方式触发一个 hook

很好,我们将会在组件挂载完毕后获取数据。但是如何使用一个 input field 去告诉 API 我们更感兴趣哪一个主题呢?我们可以将 “Redux” 被当做默认查询。那么有关于 “React” 的主题呢?下面让我们实现一个 input 元素,使人们可以获取除了 “Redux” 以外的信息。因此,我们为 input 元素引入一个新的状态。

  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;

在这个时刻,所有的状态之间都是相互独立的,但是现在你想要将它们耦合起来,通过 input field 的值来查询文章。因此有了下面的改变,组件一旦载入,就应该按照查询条件获取所有的文章。

  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;

有一个情况被忽略了:当你尝试在 input field 中输入一些东西的时候,并不会获取到数据。这是因为你提供一个空数组作为 effect 的第二个参数。effect 并不依赖于任何变量,所以只有当组件被挂载的时候才触发 effect 。而现在 effect 需要依赖于 query。当 query 改变的时候,数据请求将会再次启动。

  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;

重新获取数据将会在 input field 中的值改变的时候工作。但是这也带来了另一个问题:你输入进 input field 的每一个字符都会触发 effect 并且执行另一个数据获取请求。提供一个按钮触发请求,从而手动触发 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. }

现在,让 effect 依赖于 search state,而不是依赖于随着 input field 值改变而改变的 query state。一旦用户点击 button,新的 search state 便会被设置,并且应该手动的触发 effect。

  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;

同样的,search state 的初始值应该设置为与 query state 一样的状态,因为组件在挂载的时候依然会获取数据,因此获取的结果应该与 input field 中的值保持一致。然而,query state 和 search state 看起来很类似,那么我们为什么不设置实际的 URL 来代替 search 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. }

如果使用 effect hook 获取了一个 隐式的程序化(programmatic)数据。你可以自己决定 effect 依赖于哪一个状态。一旦你在点击时或者在另一个副作用中设置(set)了这个状态(state),effect 将会再次运行。在这个例子中,如果 URL state 改变了,effect 将会再次运行,并通过 API 来获取对应的数据。

在 React hooks 中使用 loading indicator

让我们在获取数据的时候,加入一个 loading indicator。它只是另一个被 state hook 管理的状态罢了。加载的标志常常被用来在 APP 组件中渲染一个 loading indicator。

  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;

effect 会在数据获取时被调用。当组件挂载或者是 URL state 改变的时候 React 将会获取数据,因此 effect 将被调用,此时 loading state 会被设置为 true,当请求 resolves 之后,loading state 又会被设置为 false。

在 React hooks 中处理错误

用 React hooks 获取数据是如何处理错误的呢?其实错误也只是用 state hook 初始化的另一个 state 。一旦有了错误状态,APP 组件将会给用户展示一个反馈信息。当使用 async/await,我们一般使用 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;

每一次 hook 运行的时候 ,error state 都会重置。这是很有用的操作,因为在请求失败之后,用户一般都想要再一次请求,因此我们需要充值 error 状态。为了测试我们的代码,你可以把 URL 改成一个无效的值,然后看看错误信息是否能显示出来。

使用 React 在 Forms 中获取数据

一个表单是如何获取数据的呢?到目前为止,我们只有一组 input field 和 button。当你引入更多的元素时,你可能想要去用一个 form 元素包裹它们。除此之外,form 还可以使用键盘中的 “Enter” 来触发 button。

  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. }

当时当你点击 submit button 的时候,浏览器将会自动刷新,因为提交一个表单是浏览器的一个原生行为。为了去阻止默认行为,我们可以在 React event 上调用一个函数(译者注:也就是阻止默认事件)。

  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. }

现在,当你点击 submit button 按钮之后浏览器就不会刷新了。它会像以前一样工作,只不过我们使用了 from 代替了 input field 和 button 的组合。你也可以在键盘上按下 “Enter” 来代替点击 button 按钮。

定制一个获取数据的 hook

为了制作一个定制的获取数据的 hook,我们将属于获取数据(包括 loading indicator 和 error 处理)的部分都移动到它自己的函数中去。除此之外,我们还需要确保从函数中返回所有必要的变量,这些变量将会在 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. }

现在,你定制的新 hook 就可以在 APP 组件中使用了:

  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. }

初始状态也可以被做成通用的。我们可以轻松的将它传递给新的定制的 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;

这便是使用定制的 hook 获取的数据的方式。这个 hook 本身并不关注任何的 API。它从外部接受参数并且只管理必要的状态,比如说数据,loading 以及 error 这几个状态。它执行一个请求,并且返回数据给调用了这个 hook 的组件。

REDUCER HOOK FOR DATA FETCHING

到目前为止,我们使用了各种 state hooks 去管理我们的数据获取 state,loading state 和 error state。 However, somehow all these states, managed with their own state hook, belong together because they care about the same cause. (水平有限,翻译不对味)。正如你所看到的,他们全部都在数据获取函数中被使用。一个好的实践是将它们合并到一起,也就是让他们一个接一个的被使用(例如:setIsErrorsetIsLoading)。让我们使用 Reducer Hook 来代替这三个 hooks 吧。

Reducer Hook 返回给我们一个 state object 和 一个改变 state objec 的函数。这个函数称之为 dispatch 函数,dispatch 函数接收一个 action,这个 action 有一个 type 和一个可选的 payload。这里所有的信息都在实际的 reducer 函数中被用来从先前的 state,action 中可选的 payload 和 type 中获取一个新的 state[3]。让我们通过代码看看它是如何工作的:

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

Reducer Hook 将 reducer 函数和一个初始的 state object 作为参数。在我们的例子中,初始的 data state,loading state,error state 没有改变,但是他们被合并到了一个 state 对象中并且由一个 reducer hook 代替了单个的 state hooks 来管理。

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

现在,当你要获取数据的时候,dispatch 函数被用来发送信息给 reducer 函数。传递给 dispatch 函数的对象有一个强制的 type 属性 和一个可选的 payload 属性。其中 type 属性告诉 reducer 函数需要应用哪一种状态转换,而 reducer 函数可以通过 payload 属性来提取新的状态。我们只有三个状态需要转换:初始化查询进程,提示获取数据成功以及提示获取数据失败。

在这个自定义的 hook 的最后,我们将 state 返回,不过因为我们应该返回一个 state object 而不是单独的 state。通过这种方式,调用 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. };

最后但是同样重要的是,我们还没有完成 reducer 函数。它需要扮演三个不同的 state 转换,分别是 FETCH_INIT, FETCH_SUCCESS and FETCH_FAILURE。每一个状态的转换都需要返回一个新的 state object。让我们看看如何使用 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. };

reducer 函数会通过它的参数访问到当前的 state 和 传入的 action。到目前为止,在 switch case 语句中,每一个状态变换只返回它之前的状态。我们使用解构语句来保持 state object 的不可变性 —— 这意味着 state 从来没有被直接的改变 。使用解构语句来保持 state object 的不可变性是一个最佳实践。现在让我们覆盖几个当前的状态返回的属性,以便在每一次状态转化的时候改变状态。

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

现在每一个状态的转换取决于 action 的 type,reducer函数将会基于之前的 state 和可选的 payload 返回一个新的 state 。比如,请求成功的情况下,payload 将会被设置为新的 state object 中的数据。

总之,Reducer Hook 确保了状态管理这一部分封装在了自己的逻辑之内。通过提供 action types 和可选的 payloads,you will always end up with a predicatbale state change。除此之外,你将再也不会遇到无效的状态。比如,以前可能会意外的将 isLoadingisError 设置为 true。对于这种情况 UI 该如何显示呢?现在,每一个状态转变都通过 reducer 函数定义成一个有效的 state object。

注解:

  1. 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.

ABORT DATA FETCHING IN EFFECT HOOK

在 React 中这是一个常见的问题 —— 即使组件已经被卸载了,但是组件的状态仍然会被设置(e.g. due to navigating away with React Router)。我以前写过一篇关于这个问题的文章,这篇文章描述了在各种情况下,如何防止为未挂载的组件设置状态。让我们看看在我自定义的 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. };

每一个 Effect Hook 附带一个 clean up 函数,它将会在组件被卸载的运行。clean up 函数是 effect hook 这个钩子返回的一个函数。在我们的例子中,我们使用了一个名为 didCancel 的布尔标记,让我们获取数据的逻辑知道组件的状态(挂载/卸载)。如果这个组件确实卸载了,那么标记被设置为true ,这将会导致在数据获取在 resolved 之后,无法更新组件的状态。

Note:实际上并不是数据获取被终止了——这可以通过 Axios Cancellation 来实现——但是对于未挂载的组件,状态将不会改变。因为在我看来,Axios Cancellation 并不是最好的 API ,这个布尔标记也可以胜任防止设置状态的这个功能。