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

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

在本教程中,我想向你展示如何使用 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      <input21        type="text"22        value={query}23        onChange={event => setQuery(event.target.value)}24      />25      <button type="button" onClick={() => setSearch(query)}>26        Search27      button>2829      <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      <input21        type="text"22        value={query}23        onChange={event => setQuery(event.target.value)}24      />25      <button26        type="button"27        onClick={() =>28          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)29        }30      >31        Search32      button>3334      <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      <input28        type="text"29        value={query}30        onChange={event => setQuery(event.target.value)}31      />32      <button33        type="button"34        onClick={() =>35          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)36        }37      >38        Search39      button>4041      {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}5556export 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      <input35        type="text"36        value={query}37        onChange={event => setQuery(event.target.value)}38      />39      <button40        type="button"41        onClick={() =>42          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)43        }44      >45        Search46      button>4748      {isError && <div>Something went wrong ...div>}4950      {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}6465export 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        <input12          type="text"13          value={query}14          onChange={event => setQuery(event.target.value)}15        />16        <button type="submit">Searchbutton>17      form>1819      {isError && <div>Something went wrong ...div>}2021      ...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}`); 910        event.preventDefault();11      }}>12        <input13          type="text"14          value={query}15          onChange={event => setQuery(event.target.value)}16        />17        <button type="submit">Searchbutton>18      form>1920      ...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      <form42        onSubmit={event => {43          doFetch(44            `http://hn.algolia.com/api/v1/search?query=${query}`,45          );4647          event.preventDefault();48        }}49      >50        <input51          type="text"52          value={query}53          onChange={event => setQuery(event.target.value)}54        />55        <button type="submit">Searchbutton>56      form>5758      {isError && <div>Something went wrong ...div>}5960      {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}7475export 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,
6} from '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    isLoading: false,
18    isError: false,
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    isLoading: false,
10    isError: false,
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    isLoading: false,
6    isError: false,
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        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 都有对应的处理,并且返回一个新的 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    isLoading: false,
6    isError: false,
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

42c8b1cc0f4558facefbbdea62d30aa6.png

b1cfb540060ea716f177f82713b85b30.gif戳“ 在看”的永远18岁~ dcea0c94142bfe7334c04320a66fdaf0.gif
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值