如何使用 React hooks 发送请求

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

原文地址: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 来获取数据,当然,你也可以使用别的开源库。

扫描二维码关注公众号,回复: 8630807 查看本文章
 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,
 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,因此这个防止设置状态的布尔标志也能完成这项工作。

发布了71 篇原创文章 · 获赞 0 · 访问量 1821

猜你喜欢

转载自blog.csdn.net/u011518749/article/details/103790184
今日推荐