查看原文
其他

(译) 如何使用 React hooks 获取 api 接口数据

Nealyang 全栈前端精选 2022-01-15


点击上方“蓝字”带你每天阅读全栈前端精选好文

原文地址: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 的。

1  import React, { useState } from 'react';
2
3  function App() {
4    const [data, setData] = useState({ hits: [] });
5
6    return (
7      <ul>
8        {data.hits.map(item => (
9          <li key={item.objectID}>
10            <a href={item.url}>{item.title}</a>
11          </li>
12        ))}
13      </ul>

14    );
15  }
16
17  export default App;

App 组件显示了一个项目列表(hits=Hacker News 文章)。状态和状态更新函数来自useState 的 hook。他是来负责管理我们这个 data 的状态的。userState 中的第一个值是data 的初始值。其实就是个解构赋值。

这里我们使用 axios 来获取数据,当然,你也可以使用别的开源库。

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

23  );
24}
25
26export default App;

这里我们使用 useEffect 的 effect hook 来获取数据。并且使用 useState 中的 setData 来更新组件状态。

但是如上代码运行的时候,你会发现一个特别烦人的循环问题。effect hook 的触发不仅仅是在组件第一次加载的时候,还有在每一次更新的时候也会触发。由于我们在获取到数据后就进行设置了组件状态,然后又触发了 effect hook。所以就会出现死循环。很显然,这是一个 bug!我们只想在组件第一次加载的时候获取数据 ,这也就是为什么你可以提供一个空数组作为 useEffect 的第二个参数以避免在组件更新的时候也触发它。当然,这样的话,也就是在组件加载的时候触发。

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

23    );
24  }
25
26  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的原因。但是我们可以通过如下方法解决:

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

27    );
28  }
29
30  export default App;

如上就是通过 React hooks 来获取 API 数据。但是,如果你对错误处理、loading、如何触发从表单中获取数据或者如何实现可重用的数据获取的钩子。请继续阅读。

如何自动或者手动的触发 hook?(How to trigger a hook programmatically/manually?)

目前我们已经通过组件第一次加载的时候获取了接口数据。但是,如何能够通过输入的字段来告诉 api 接口我对那个主题感兴趣呢?(就是怎么给接口传数据。这里原文说的有点啰嗦(还有 redux 关键字来混淆视听),我直接上代码吧)…

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

这里我跳过一段,原文实在说的太细了。

缺少一件:当你尝试输入字段键入内容的时候,他是不会再去触发请求的。因为你提供的是一个空数组作为useEffect的第二个参数是一个空数组,所以effect hook 的触发不依赖任何变量,因此只在组件第一次加载的时候触发。所以这里我们希望当 query 这个字段一改变的时候就触发搜索

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

如上,我们只是把 query作为第二个参数传递给了 effect hook,这样的话,每当 query 改变的时候就会触发搜索。但是,这样就会出现了另一个问题:每一次的query 的字段变动都会触发搜索。如何提供一个按钮来触发请求呢?

1function App() {
2  const [data, setData] = useState({ hits: [] });
3  const [query, setQuery] = useState('redux');
4  const [search, setSearch] = useState('redux');
5
6  useEffect(() => {
7    const fetchData = async () => {
8      const result = await axios(
9        `http://hn.algolia.com/api/v1/search?query=${search}`,
10      );
11
12      setData(result.data);
13    };
14
15    fetchData();
16  }, [search]);
17
18  return (
19    <Fragment>
20      <input
21        type="text"
22        value={query}
23        onChange={event => setQuery(event.target.value)}
24      />
25      <button type="button" onClick={() => setSearch(query)}>
26        Search
27      </button>
28
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}
39

搜索的状态设置为组件的初始化状态,组件加载的时候就要触发搜索,类似的查询和搜索状态易造成混淆,为什么不把实际的 URL 设置为状态而不是搜索状态呢?

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

这是一个使用 effect hook 来获取数据的一个例子,你可以决定 effect hook 所以依赖的状态。一旦你点击或者其他的什么操作 setState 了,那么 effect hook 就会运行。但是这个例子中,只有当你的 url 发生变化了,才会再次去获取数据。

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

这里让我们来给程序添加一个 loading(加载器),这里需要另一个 state

1import React, { Fragment, useState, useEffect } from 'react';
2import axios from 'axios';
3
4function App() {
5  const [data, setData] = useState({ hits: [] });
6  const [query, setQuery] = useState('redux');
7  const [url, setUrl] = useState(
8    'https://hn.algolia.com/api/v1/search?query=redux',
9  );
10  const [isLoading, setIsLoading] = useState(false);
11
12  useEffect(() => {
13    const fetchData = async () => {
14      setIsLoading(true);
15
16      const result = await axios(url);
17
18      setData(result.data);
19      setIsLoading(false);
20    };
21
22    fetchData();
23  }, [url]);
24
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
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
56export default App;
57

代码比较简单,不解释了

使用 Effect Hook 添加错误处理(Error Handling with React Hooks)

如何在 Effect Hook 中做一些错误处理呢?错误仅仅是一个 state ,一旦程序出现了 error state,则组件需要去渲染一些feedback 给用户。当我们使用 async/await 的时候,我们可以使用try/catch,如下:

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

每一次 effect hook 运行的时候都需要重置一下 error state,这是非常有必要的。因为用户可能想再发生错误的时候想再次尝试一下。

说白了,界面给用户反馈更加的友好

使用 React 中 Form 表单获取数据(Fetching Data with Forms and React)

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

为了防止浏览器的 reload,我们这里加了一个event.preventDefalut(),然后别的操作就是正常表单的操作了

自定义获取数据的 hook(Custom Data Fetching Hook)

其实就是请求的封装

为了能够提取自定义的请求 hook,除了属于输入框的 query 字段,别的包括 loading 加载器、错误处理函数都要包括在内。当然,你需要确保 App Component 所需的所有字段在你自定义的 hook 中都有返回

1const 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
9  useEffect(() => {
10    const fetchData = async () => {
11      setIsError(false);
12      setIsLoading(true);
13
14      try {
15        const result = await axios(url);
16
17        setData(result.data);
18      } catch (error) {
19        setIsError(true);
20      }
21
22      setIsLoading(false);
23    };
24
25    fetchData();
26  }, [url]);
27
28  return [{ data, isLoading, isError }, setUrl];
29}

现在,我们可以将你的新 hook 继续放到组件中使用

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

通常我们需要一个初始状态。将它简单的传递给自定义 hook 中

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

如上,就是我们使用自定义 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 一毛一样

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

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

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

现在,在获取数据的时候,可以使用 dispathc function 来给reducer传递参数。使用dispatch函数发送的对象具有必需的type属性和可选的payload属性。该类型告诉reducer功能需要应用哪个状态转换,并且reducer可以另外使用有效负载来提取新状态。毕竟,我们只有三个状态转换:初始化提取过程,通知成功的数据提取结果,并通知错误的数据提取结果。

在我们自定义的 hook 中,state 像以前一样返回。但是因为我们有一个状态对象而不是独立状态。这样,调用useDataApi自定义钩子的人仍然可以访问数据,isLoading和isError:

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

最后还有我们 reducer 函数的实现。它需要作用于三个不同的状态转换,称为FETCH_INIT,FETCH_SUCCESS和FETCH_FAILURE。每个状态转换都需要返回一个新的状态对象。让我们看看如何使用switch case语句实现它:

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

现在,每一个 action 都有对应的处理,并且返回一个新的 state。

总之,Reducer Hook确保状态管理的这一部分用自己的逻辑封装。此外,你永远不会遇到无效状态。例如,以前可能会意外地将isLoading和isError状态设置为true。在这种情况下,UI应该显示什么?现在,reducer函数定义的每个状态转换都会导致一个有效的状态对象。

在 Effect Hook 中 中止数据请求(Abort Data Fetching in Effect Hook)

React中的一个常见问题是,即使组件已经卸载(例如由于使用React Router导航),也会设置组件状态。我之前已经在这里写过关于这个问题的文章,它描述了如何防止在各种场景中为未加载的组件中设置状态。让我们看看我们如何阻止在数据提取的自定义钩子中设置状态:

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

每一个 Effect Hook 都自带一个清理功能。该功能在组件卸载时运行。清理功能是 hook 返回的一个功能。在我们的例子中,我们使用一个名为 didCancel 的 boolean 来标识组件的状态。如果组件已卸载,则该标志应设置为true,这将导致在最终异步解析数据提取后阻止设置组件状态。

注意:实际上不会中止数据获取 - 这可以通过Axios Cancellation实现 - 但是对于 unmounted 的组件不再执行状态转换。由于Axios Cancellation在我看来并不是最好的API,因此这个防止设置状态的布尔标志也能完成这项工作。

往期精彩回顾从 URL 输入到页面展现到底发生了什么
你未必知道的 49 个 css 知识点学习方法分享:为何我一年半就能拿到大厂 offer


“阅读原文”,获取原文地址(能查阅外链)“在看”的永远18岁~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存