在 React Hooks 中如何请求数据?

image

经过这个教程,我想告诉你在 React 中如何使用 state 和 effect 这两种 hooks 去请求数据。咱们将使用众所周知的 Hacker News API 来获取一些热门文章。你将定义属于你本身的数据请求的 Hooks ,而且能够在你全部的应用中复用,也能够发布到 npm 。react

若是你不了解 React 的这些新特性,能够查看个人另外一篇文章 introduction to React Hooks。若是你想直接查看文章的示例,能够直接 checkout 这个 Github 仓库ios

注意:在 React 将来的版本中,Hooks 将不会用了获取数据,取而代之的是一种叫作 Suspense 的东西。尽管如此,下面的方法依然是了解 state 和 effect 两种 Hooks 的好方法。

使用 React Hooks 进行数据请求

若是你没有过在 React 中进行数据请求的经验,能够阅读个人文章:How to fetch data in React。文章讲解了如何使用 Class components 获取数据,如何使用可重用的 Render Props Components 和 Higher Order Components ,以及如何进行错误处理和 loading 状态。在本文中,我想用 Function components 和 React Hooks 来重现这一切。git

import React, { useState } from 'react';

function App() {
  const [data, setData] = useState({ hits: [] });

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

App 组件将展现一个列表,列表信息来自 Hacker News articles 。状态和状态更新函数将经过被称为 useState 的状态钩子来生成,它负责管理经过请求获得的 App 组件的本地状态。初始状态是一个空数组,目前没有任何地方给它设置新的状态。github

咱们将使用 axios 来获取数据,固然也可使用你熟悉的请求库,或者浏览器自带的 fetch API。若是你尚未安装过 axios ,能够经过 npm install axios 进行安装。npm

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'http://hn.algolia.com/api/v1/search?query=redux',
    );

    setData(result.data);
  });

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

咱们在 useEffect 这个 effect hook 中,经过 axios 从 API 中获取数据,并使用 state hook 的更新函数,将数据存入到本地 state 中。而且使用 async/await 来解析promise。redux

然而,当你运行上面的代码的时候,你会陷入到该死的死循环中。effect hook 在组件 mount 和 update 的时候都会执行。由于咱们每次获取数据后,都会更新 state,因此组件会更新,并再次运行 effect,这会一次又一次的请求数据。很明显咱们须要避免这样的bug产生,咱们只想在组件 mount 的时候请求数据。你能够在 effect hook 提供的第二个参数中,传入一个空数组,这样作能够避免组件更新的时候执行 effect hook ,可是组件在 mount 依然会执行它。axios

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'http://hn.algolia.com/api/v1/search?query=redux',
    );

    setData(result.data);
  }, []);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

第二个参数是用来定义 hook 因此依赖的变量的。若是其中一个变量发生变化,hook 将自动运行。若是第二个参数是一个空数组,那么 hook 将不会在组件更新是运行,由于它没有监控任何的变量。api

还有一个须要特别注意的点,在代码中,咱们使用了 async/await 来获取第三方 API 提供的数据。根据文档,每个 async 函数都将返回一个隐式的 promise:数组

"The async function declaration defines an asynchronous function, which returns an AsyncFunction object. An asynchronous function is a function which operates asynchronously via the event loop, using an implicit Promise to return its result. "

“async 函数定义了一个异步函数,它返回的是一个异步函数对象,异步函数是一个经过事件循环进行操做的函数,使用隐式的 Promise 返回最终的结果。”promise

然而,effect hook 应该是什么也不返回的,或者返回一个 clean up 函数的。这就是为何你会在控制台看到一个错误信息。

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 hook 中使用 async 函数。

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'http://hn.algolia.com/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

这就是一个使用 React Hooks 进行数据请求的小案例。可是,若是你对错误处理、loading 态、如何触发表单数据获取以及如何复用出具处理 hook 感兴趣,那咱们接着往下看。

如何手动或者自动触发一个 hook?

如今咱们已经可以在组件 mount 以后获取到数据,可是,如何使用输入框动态告诉 API 选择一个感兴趣的话题呢?能够看到以前的代码,咱们默认将 "Redux" 做为查询参数('http://hn.algolia.com/api/v1/...'),可是咱们怎么查询关于 React 相关的话题呢?让咱们实现一个 input 输入框,能够得到除了 “Redux” 以外的其余的话题。如今,让咱们为输入框引入一个新的 state。

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'http://hn.algolia.com/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

export default App;

如今,请求数据和查询参数两个 state 相互独立,可是咱们须要像一个办法但愿他们耦合起来,只获取输入框输入的参数指定的话题文章。经过如下修改,组件应该在 mount 以后按照查询获取相应文章。

...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    ...
  );
}

export default App;

实际上,咱们还缺乏部分代码。你会发现当你在输入框输入内容后,并无获取到新的数据。这是由于 useEffect 的第二个参数只是一个空数组,此时的 effect 不依赖于任何的变量,因此这只会在 mount 只会触发一次。可是,如今咱们须要依赖查询条件,一旦查询发送改变,数据请求就应该再次触发。

...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [query]);

  return (
    ...
  );
}

export default App;

好了,如今一旦你改变输入框内容,数据就会从新获取。可是如今又要另一个问题:每次输入一个新字符,就会触发 effect 进行一次新的请求。那么咱们提供一个按钮来手动触发数据请求呢?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${search}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [search]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

此外,search state 的初始状态也是设置成了与 query state 相同的状态,由于组件在 mount 的时候会请求一次数据,此时的结果也应该是反应的是输入框中的搜索条件。然而, search state 和 query state 具备相似的值,这看起来比较困惑。为何不将真实的 URL 设置到 search state 中呢?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(url);

      setData(result.data);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

这就是经过 effect hook 获取数据的案例,你能够决定 effect 取决于哪一个 state。在这个案例中,若是 URL 的 state 发生改变,则再次运行该 effect 经过 API 从新获取主题文章。

Loading 态 与 React Hooks

让咱们在数据的加载过程当中引入一个 Loading 状态。它只是另外一个由 state hook 管理的状态。Loading state 用于在 App 组件中呈现 Loading 状态。

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);

      const result = await axios(url);

      setData(result.data);
      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

如今当组件处于 mount 状态或者 URL state 被修改时,调用 effect 获取数据,Loading 状态就会变成 true。一旦请求完成,Loading 状态就会再次被设置为 false。

错误处理与 React Hooks

经过 React Hooks 进行数据请求时,如何进行错误处理呢? 错误只是另外一个使用 state hook 初始化的另外一种状态。一旦出现错误状态,App 组件就能够反馈给用户。当使用 async/await 函数时,一般使用 try/catch 来进行错误捕获,你能够在 effect 中进行下面操做:

...

const [isError, setIsError] = useState(false);

useEffect(() => {
  const fetchData = async () => {
    setIsError(false);
    setIsLoading(true);
    
    try {
      const result = await axios(url);
      setData(result.data);
    } catch (error) {
      setIsError(true);
    }
    
    setIsLoading(false);
  };

  fetchData();
}, [url]);

return (
  <Fragment>
    ...
    {isError && <div>Something went wrong ...</div>}
    ...
  <Fragment>
);

effect 每次运行都会重置 error state 的状态,这颇有用,由于每次请求失败后,用户可能从新尝试,这样就可以重置错误。为了观察代码是否生效,你能够填写一个无用的 URL ,而后检查错误信息是否会出现。

使用表单进行数据获取

什么才是获取数据的正确形式呢?如今咱们只有输入框和按钮进行组合,一旦引入更多的 input 元素,你可能想要使用表单来进行包装。此外表单还可以触发键盘的 “Enter” 事件。

function App() {
  ...
  const doFetch = (evt) => {
    evt.preventDefault();
    setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
  }
  return (
    <Fragment>
      <form
        onSubmit={ doFetch }
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}

自定义 hook 获取数据

咱们能够定义一个自定义的 hook,提取出全部与数据请求相关的东西,除了输入框的 query state,除此以外还有 Loading 状态、错误处理。还要确保返回组件中须要用到的变量。

const useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  const doFetch = () => {
    setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
  };

  return { data, isLoading, isError, doFetch };
}

如今,咱们在 App 组件中使用咱们的新 hook 。

function App() {
  const [query, setQuery] = useState('redux');
  const { data, isLoading, isError, doFetch } = useHackerNewsApi();

  return (
    <Fragment>
      ...
    </Fragment>
  );
}

接下来,在外部传递 URL 给 DoFetch 方法。

const useHackerNewsApi = () => {
  ...

  useEffect(
    ...
  );

  const doFetch = url => {
    setUrl(url);
  };

  return { data, isLoading, isError, doFetch };
};

function App() {
  const [query, setQuery] = useState('redux');
  const { data, isLoading, isError, doFetch } = useHackerNewsApi();

  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(
            `http://hn.algolia.com/api/v1/search?query=${query}`,
          );

          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      ...
    </Fragment>
  );
}

初始的 state 也是通用的,能够经过参数简单的传递到自定义的 hook 中:

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

const useDataApi = (initialUrl, initialData) => {
  const [data, setData] = useState(initialData);
  const [url, setUrl] = useState(initialUrl);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  const doFetch = url => {
    setUrl(url);
  };

  return { data, isLoading, isError, doFetch };
};

function App() {
  const [query, setQuery] = useState('redux');
  const { data, isLoading, isError, doFetch } = useDataApi(
    'http://hn.algolia.com/api/v1/search?query=redux',
    { hits: [] },
  );

  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(
            `http://hn.algolia.com/api/v1/search?query=${query}`,
          );

          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

这就是使用自定义 hook 获取数据的方法,hook 自己对API一无所知,它从外部获取参数,只管理必要的 state ,如数据、 Loading 和错误相关的 state ,而且执行请求并将数据经过 hook 返回给组件。

用于数据获取的 Reducer Hook

目前为止,咱们已经使用 state hooks 来管理了咱们获取到的数据数据、Loading 状态、错误状态。然而,全部的状态都有属于本身的 state hook,可是他们又都链接在一块儿,关心的是一样的事情。如你所见,全部的它们都在数据获取函数中被使用。它们一个接一个的被调用(好比:setIsErrorsetIsLoading),这才是将它们链接在一块儿的正确用法。让咱们用一个 Reducer Hook 将这三者链接在一块儿。

Reducer Hook 返回一个 state 对象和一个函数(用来改变 state 对象)。这个函数被称为分发函数(dispatch function),它分发一个 action,action 具备 type 和 payload 两个属性。全部的这些信息都在 reducer 函数中被接收,根据以前的状态提取一个新的状态。让咱们看看在代码中是如何工做的:

import React, {
  Fragment,
  useState,
  useEffect,
  useReducer,
} from 'react';
import axios from 'axios';

const dataFetchReducer = (state, action) => {
  ...
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...
};

Reducer Hook 以 reducer 函数和一个初始状态对象做为参数。在咱们的案例中,加载的数据、Loading 状态、错误状态都是做为初始状态参数,且不会发生改变,可是他们被聚合到一个状态对象中,由 reducer hook 管理,而不是单个 state hooks。

const dataFetchReducer = (state, action) => {
  ...
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
      } catch (error) {
        dispatch({ type: 'FETCH_FAILURE' });
      }
    };

    fetchData();
  }, [url]);

  ...
};

如今,在获取数据时,可使用 dispatch 函数向 reducer 函数发送信息。使用 dispatch 函数发送的对象具备一个必填的 type 属性和一个可选的 payload 属性。type 属性告诉 reducer 函数须要转换的 state 是哪一个,还能够从 payload 中提取新的 state。在这里只有三个状态转换:初始化数据过程,通知数据请求成功的结果,以及通知数据请求失败的结果。

在自定义 hook 的末尾,state 像之前同样返回,可是由于咱们全部的 state 都在一个对象中,而再也不是独立的 state ,因此 state 对象进行解构返回。这样,调用 useDataApi 自定义 hook 的人仍然能够 dataisLoadingisError:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...

  const doFetch = url => {
    setUrl(url);
  };

  return { ...state, doFetch };
};

最后咱们还缺乏 reducer 函数的实现。它须要处理三个不一样的状态转换,分被称为 FEATCH_INITFEATCH_SUCCESSFEATCH_FAILURE。每一个状态转换都须要返回一个新的状态。让咱们看看使用 switch case 如何实现这个逻辑:

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return { ...state };
    case 'FETCH_SUCCESS':
      return { ...state };
    case 'FETCH_FAILURE':
      return { ...state };
    default:
      throw new Error();
  }
};

reducer 函数能够经过其参数访问当前状态和 dispatch 传入的 action。到目前为止,在 switch case 语句中,每一个状态转换只返回前一个状态,析构语句用于保持 state 对象不可变(即状态永远不会被直接更改)。如今让咱们重写一些当前 state 返回的属性,以便在每次转换时更改 一些 state:

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

如今,每一个状态转换(action.type决定)都返回一个基于先前 state 和可选 payload 的新状态。例如,在请求成功的状况下,payload 用于设置新 state 对象的 data 属性。

总之,reducer hook 确保使用本身的逻辑封装状态管理的这一部分。经过提供 action type 和可选 payload ,老是会获得可预测的状态更改。此外,永远不会遇到无效状态。例如,之前可能会意外地将 isLoadingisError 设置为true。在这种状况下,UI中应该显示什么? 如今,由 reducer 函数定义的每一个 state 转换都指向一个有效的 state 对象。

在 Effect Hook 中中断数据请求

在React中,即便组件已经卸载,组件 state 仍然会被被赋值,这是一个常见的问题。我在以前的文章中写过这个问题,它描述了如何防止在各类场景中为未挂载组件设置状态。让咱们看看在自定义 hook 中,请求数据时如何防止设置状态:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

        if (!didCancel) {
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [url]);

  const doFetch = url => {
    setUrl(url);
  };

  return { ...state, doFetch };
};

每一个Effect Hook都带有一个clean up函数,它在组件卸载时运行。clean up 函数是 hook 返回的一个函数。在该案例中,咱们使用 didCancel 变量来让 fetchData 知道组件的状态(挂载/卸载)。若是组件确实被卸载了,则应该将标志设置为 true,从而防止在最终异步解析数据获取以后设置组件状态。

注意:实际上并无停止数据获取(不过能够经过Axios取消来实现),可是再也不为卸载的组件执行状态转换。因为 Axios 取消在我看来并非最好的API,因此这个防止设置状态的布尔标志也能够完成这项工做。

原文连接

相关文章
相关标签/搜索