阅读 1251

利用React 自定义Hooks实现业务逻辑复用实例

简介

React Hooks 除了可以给函数式组件赋予状态和生命周期管理外,最重要的一点是可以把共用的逻辑处理代码抽离出来给不同的视图组件进行使用,它的目的和 HOCRender Props 基本相同。这篇文章将以请求远程API数据为例,最后抽离出公共的数据请求的自定义 hook 以便不同的组件需要获取数据时,可以共用一套逻辑。

适用读者

本文的适用读者为:

请求数据的例子

本文的完整代码和运行示例请点击下方按钮查看:


更完善的 useRequest Hook 可以参考我的开源项目:

需求

假设我们有一个组件,需要同时加载 Posts (博客文章) 和 Todos (待办事项) 列表,每种展示5条标题,并根据加载状态显示loading组件。首先我们看使用一般hooks的方式。

定义 States

首先我们定义需要的 states:

  // PostsAndTodos.js
  const [posts, setPosts] = useState([]);
  const [isPostsLoading, setIsPostsLoading] = useState();
  const [todos, setTodos] = useState([]);
  const [isTodosLoading, setIsTodosLoading] = useState();
复制代码
  • posts 用来保存远程加载的文章数据,isPostsLoading 保存文章数据的加载状态
  • todos 保存待办事项,isTodosLoading保存待办事项数据的加载状态
  • 这里我们定义了4个类似的状态

加载 Posts

我们使用 useEffect 来请求 posts 数据:

  // PostsAndTodos.js
  useEffect(() => {
    const loadPosts = async () => {
      setIsPostsLoading(true);
      try {
        let response = await fetch(
          "https://jsonplaceholder.typicode.com/posts?_limit=5"
        );
        let data = await response.json();
        setPosts(data);
      } catch (e) {
        console.log(e);
      }

      setIsPostsLoading(false);
    };
    loadPosts();
  }, []);
复制代码
  1. 我们首先在 useEffect 里定义了 loadPosts() 函数,用来请求远程数据。
  2. loadPosts()函数里,我们在请求开始,设置加载状态为true,在结束时,无论是否出错,都把加载状态设置为false
  3. 然后函数中间,我们发起请求,把返回的数据更新到 state 中。

加载 Todos

加载 todos 的代码和加载 posts 的代码基本相同,只是请求的 url 和更新状态的函数名不同:

  // PostsAndTodos.js
  useEffect(() => {
    const loadTodos = async () => {
      setIsTodosLoading(true);
      try {
        let response = await fetch(
          "https://jsonplaceholder.typicode.com/todos?_limit=5"
        );
        let data = await response.json();
        setTodos(data);
      } catch (e) {
        console.log(e);
      } finally {
        setIsTodosLoading(false);
      }
    };
    loadTodos();
  }, []);
复制代码

展示数据

我们这里简单的使用 <ul><li> 展示 poststodos 的列表:

<div>
  <h1>Posts</h1>
  <ul>
    {isPostsLoading ? (
      <div>loading...</div>
    ) : (
      posts.map(post => <li key={post.id}>{post.title}</li>)
    )}
  </ul>
  <h1>Todos</h1>
  <ul>
    {isTodosLoading ? (
      <div>loading...</div>
    ) : (
      todos.map(todo => <li key={todo.id}>{todo.title}</li>)
    )}
  </ul>
</div>
复制代码

问题

这样做问题显而易见,我们把请求数据的逻辑基本上重复了两遍,唯一不同的就是请求的 url,这样不符合可复用的代码设计规范,而且这个组件充斥着大量的业务逻辑和展示逻辑代码,十分臃肿,也违背了组件分离的设计原则,即负责展示的组件应当和业务处理的代码分离。那么如何进行优化呢?

用自定义 Hooks 改造

使用 React 自定义 Hooks,我们可以把请求数据、更新加载状态、更新错误状态的代码全部写到自定义的 Hooks 中,然后在需要它的组件中调用即可,一般只需要一行代码。接下来我们就对上边的例子进行改造。

useRequest

我们新建一个useRequest.js的文件,然后定义一个同名的函数:

function useRequest(url) {
   // 代码
   
   // return ...
}
复制代码

React 官方推荐自定义的 Hooks 使用 use 开头,因为这样的命名规范使得阅读代码的人知道使用它可以做什么事情。这个函数有如下特点:

  • 函数可以接受参数,可以是任何此 hook 所需要的值,甚至是其他 hooks 返回的值。
  • 函数可以有返回值,给任何调用此 hook 的组件使用

接下来我们编写它的业务逻辑,其实在使用自定义 Hooks 多次之后就会发现,函数式组件里的第一行代码到 return 之前的代码都可以直接放到自定义 Hooks 里边,因为它们都属于业务代码,而 return 中的代码负责展示逻辑。

我们复制加载 posts 或者 todos 的代码到 useRequest 的函数中,然后稍加改动:

  // useRequest.js
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState();
  const [error, setError] = useState();

  useEffect(() => {
    const loadData = async () => {
      setIsLoading(true);
      try {
        let response = await fetch(url);
        let data = await response.json();
        setData(data);
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false);
      }
    };
    loadData();
  }, []);

  return [data, isLoading, error];
复制代码

这里我们把状态和数据的命名改为了更通用的 data, isLoading, 和 error,因为我们不限制特定 domain,任何需要加载数据的组件都可以使用此 hook。在函数的最后我们返回了 data, isLoading, error 这三个状态供调用此 hook 的组件使用,这里返回值的类型没有要求,如果返回值比较多,也可以返回一个对象,如:

  return {data, isLoading, error}
复制代码

加载 Posts 和 Todos

现在我们再看看加载数据的代码,在新的 PostsAndTodosWithHooks.js 文件中,我们只需要在组件里调用 useRequest hook 即可进行数据抓取:

// PostsAndTodosWithHooks.js
const [posts, isPostsLoading] = useRequest(
    "https://jsonplaceholder.typicode.com/posts?_limit=5"
);
const [todos, isTodosLoading] = useRequest(
    "https://jsonplaceholder.typicode.com/todos?_limit=5"
);

复制代码
  • useRequest hook 需要一个 url 参数,即请求的数据的地址。
  • 我们接收了返回的数据和加载状态返回值,利用了ES6的语法,然后把 dataisLoading 起了别名。
  • useRequest hook中的 dataisLoading 更新时,PostsAndTodos 组件中用到这两个状态的值也会同步更新。
  • 组件渲染部分的代码不变。

运行代码,效果和之前一致,但是代码简洁很多,hook 和展示组件各司其职,使得业务逻辑和视图的代码都变得清晰易读。而且复用业务逻辑,也使得代码量大大减少,此例则直接减少了50%。

状态隔离

可能会有人好奇这两次调用同一个 Hook 状态不会冲突吗?如果 posts 先加载完,那么 todos 会不会在加载完成之后覆盖 posts 的数据?答案是不会。不同的 hook 的调用,所有的状态都是隔离的,即使是同一 hook 调用了多次,它们也都各自维护自己的状态,互不影响。当 hook 中的状态更新时,会同步到调用它的组件中。

扩展

大家可以在本示例的基础上进行扩展,比如加上请求方式、请求头、请求参数之类的变量,还可以加上缓存数据,停止请求或重新触发请求的代码,另外像组件的事件处理(如分页)都可定义到自定义的 hooks 中,然后所有需要(如分页)的组件都可以使用它完成相应的功能。

总结

React 自定义 Hooks 提供了极其重要的功能,可以让业务逻辑从展示组件中抽离出来,并可以多次复用,极大的减少了代码量,提高了效率。掌握它会对前端开发有巨大的帮助,在这里回顾一下它的特点和注意事项:

  • 自定义 hook 以 use 开头
  • 可以接受参数
  • 可以返回任何类型的返回值
  • 可以使用其他内置或自定义的 hooks
  • 每次调用 hook,其中的状态和逻辑都是隔离的

你学会了吗?有问题欢迎留言。