封装请求需要考虑到的点:请求发起与监听、错误处理、loading spinners。

简易请求

简易的请求

  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://example.com/api',
  8. );
  9. setData(result.data);
  10. }, []);
  11. return (
  12. <ul>
  13. {data.hits.map(item => (
  14. <li key={item.id}>
  15. <a href={item.url}>{item.title}</a>
  16. </li>
  17. ))}
  18. </ul>
  19. );
  20. }
  21. export default App;

useEffect 的第二个参数添加依赖项之后,只有当依赖发生变更之后,useEffect 才会再次执行。现在我们输入的参数为空,那就只有在组件首次创建的时候才会执行。
useEffect 的第一个参数中使用 async 函数是不被允许的,但我们可以在其内部进行定义。

  1. function App () {
  2. // ...
  3. useEffect(() => {
  4. const fetchData = async () => {
  5. const result = await axios('https://example.com/api')
  6. setData(result.data)
  7. }
  8. fetchData()
  9. }, [])
  10. // ...
  11. }

手动添加依赖

如果想手动触发请求,这时我们可以为 useEffect 来添加实际的依赖,然后手动触发对这个依赖进行修改,进而触发实际请求。

  1. import React, { 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://example.com/api?query=${query}`,
  10. );
  11. setData(result.data);
  12. };
  13. fetchData();
  14. }, [query]);
  15. return (
  16. <>
  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.id}>
  25. <a href={item.url}>{item.title}</a>
  26. </li>
  27. ))}
  28. </ul>
  29. </>
  30. );
  31. }
  32. export default App;

我们将请求的参数 query 作为依赖进行添加,这样当这个参数发生改变的时候,useEffect 就可以再次触发,从而发起新的请求。

添加触发按钮

进一步,我们可以添加一个手动触发的按钮。

  1. function App() {
  2. const [data, setData] = useState({ hits: [] });
  3. const [query, setQuery] = useState('redux');
  4. const [url, setUrl] = useState(
  5. 'http://example.com/api?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. <>
  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://example.com/api?query=${query}`)
  25. }
  26. >
  27. Search
  28. </button>
  29. <ul>
  30. {data.hits.map(item => (
  31. <li key={item.id}>
  32. <a href={item.url}>{item.title}</a>
  33. </li>
  34. ))}
  35. </ul>
  36. </>
  37. );
  38. }

我们新增一个 state,用来存放 query 的值,当点击按钮的时候手动对其进行赋值,进而触发 useEffect 的再次执行。相当于是间接对依赖项进行修改,从而触发请求。

添加 loading 和请求状态

  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. 'http://example.com/api?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://example.com/api?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.id}>
  40. <a href={item.url}>{item.title}</a>
  41. </li>
  42. ))}
  43. </ul>
  44. )}
  45. </Fragment>
  46. );
  47. }
  48. export default App;

这里我们新增了一个 loading 状态,根据请求的阶段对其进行操作,从而来标明当前的状态。

添加错误处理

  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. 'http://example.com/api?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. <>
  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://example.com/api?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. </>
  53. );
  54. }
  55. export default App;

这里我们新增了一个 isError 的 state 来表明请求的状态,并且增加了错误处理。

自定义请求 hook

我们可以将关于请求的部分进行抽离,做成一个单独的 hook 函数。

  1. const useFetch = (initUrl, initData) => {
  2. const [data, setData] = useState(initData);
  3. const [url, setUrl] = useState(initUrl);
  4. const [isLoading, setIsLoading] = useState(false);
  5. const [isError, setIsError] = useState(false);
  6. useEffect(() => {
  7. const fetchData = async () => {
  8. setIsError(false);
  9. setIsLoading(true);
  10. try {
  11. const result = await axios(url);
  12. setData(result.data);
  13. } catch (error) {
  14. setIsError(true);
  15. }
  16. setIsLoading(false);
  17. };
  18. fetchData();
  19. }, [url]);
  20. return [{ data, isLoading, isError }, setUrl];
  21. }
  1. function App () {
  2. const [query, setQuery] = useState('redux')
  3. const [{ data, isLoading, isError }, doFetch] = useFetch()
  4. return (
  5. <>
  6. <form onSubmit={evt => {
  7. doFetch(`http://example.com/api?query=${query}`)
  8. evt.preventDefault()
  9. }}>
  10. <input type="text"
  11. value={query}
  12. onChaneg={evt => setQuery(evt.target.value)}
  13. />
  14. <button type="submit">Search</button>
  15. </form>
  16. { isError && <div>出错了。。。</div> }
  17. {
  18. isLoading ? (<div>加载中。。。</div>)
  19. : (
  20. <ul>
  21. {data.hits.map(item => (
  22. <li key={item.id}>
  23. <a href={item.url}>{ item.title }</a>
  24. </li>
  25. ))}
  26. </ul>
  27. )
  28. }
  29. </>
  30. )
  31. }

这样我们就完成了逻辑的抽离,将请求函数进行单独封装,而不需要使用往常的高阶组件来对组件类上面添加额外的方法。

使用 useReducer

在上面的例子中,我们使用了多个自定义的 state 来处理请求。这些独立的 state 都是为了同一个目的而存在。我们可以使用 useReducer 将它们统一起来进行处理。这里相当于我们在局部创建了一个 redux。

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

这里我们创建了一个 reducer,将请求分为不同的状态,从而将多个独立的 state 统一起来。提高处理的效率。

  1. const useFetch = (initUrl, initData) => {
  2. const [url, setUrl] = useState(initUrl)
  3. const [state, dispatch] = useReducer(dataFetchReducer, {
  4. isLoading: false,
  5. isError: false,
  6. data: initData
  7. })
  8. useEffect(() => {
  9. const fetchData = async () => {
  10. dispatch({ type: 'FETCH_INIT' })
  11. try {
  12. const result = await axios(url)
  13. dispatch({ type: 'FETCH_SUCCESS', payload: result.data })
  14. } catch (err) {
  15. dispatch({ type: 'FETCH_FAILURE' })
  16. }
  17. }
  18. })
  19. return [state, setUrl]
  20. }

添加取消请求的操作

  1. const useFetch = (initUrl, initData) => {
  2. const [url, setUrl] = useState(initUrl);
  3. const [state, dispatch] = useReducer(dataFetchReducer, {
  4. isLoading: false,
  5. isError: false,
  6. data: initData,
  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. };

useEffect 可以返回一个清除函数,它会在组件 unmount 的时候执行。因为我们的请求是异步操作,会在第17行 await 一段时间,如果在请求结果返回之前 unmout。那么 didCancel 就会变成 true,当结果返回时就不会去执行 FETCH_SUCCESS 或者 FETCH_FAILURE。