likes
comments
collection

解放劳动力,玩转自定义 Hook

作者站长头像
站长
· 阅读数 9

起因

codeReview
某前端大佬:小白呀,你这段代码好眼熟,怎么到处都有?
小白:......
某前端大佬:小白呀,你这个功能可以封住起来,其它地方也需要
小白:......
某前端大佬:小白呀,你这个小需求,怎么写了两千行代码?
小白:......

小白被问得一脸问号???决定疼定思过,好好优化一番。 Hook 是 React 16.8 的新增特性,正式发布以来,越来越多的项目正在使用函数式组件代替class类组件,Hooks 这一新特性也逐渐被广泛的使用。 然而在开发的过程中,我们发现大部分逻辑是重复且可被复用的,如对数据请求的逻辑处理,状态处理等,同样的代码经常会在不同的文件出现,以至于导致小白被问得一脸问号?

解法

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

Hooks 对于新手而言很友好,解决了 Class 组件的 this 、 生命周期等学习成本过高的问题,但是也引入了一些规则,如:不要在循环,条件或嵌套函数中调用 Hook,导致可能会遇到一些奇奇怪怪的问题而摸掉几根头发。

自定义 Hook

以获取数据为列

我们假设现在需要获取一个用户信息,你可能会这样子写:

const [user, setUser] = useState<IUser>({
    name: "",
    age: 0,
  });

  const getUserInfo = () => {
    // 模拟数据请求
    const promise = new Promise<IUser>(function (resolve, reject) {
      setTimeout(() => {
        resolve({
          name: "小白",
          age: 18,
        });
      }, 1000);
    });
    promise.then((res: IUser) => {
      setUser(res);
    });
  };

  return (
    <div>
      <Button onClick={getUserInfo}>获取用户信息</Button>
      <br />
      <div> name: {user.name}</div>
      <div> age: {user.age}</div>
    </div>
  );

如果我们需要在另一个地方也需要获取用户信息,可以把上面类似的逻辑复制并粘贴到组件中来,但这并不是理想的解决方案。 相反,我没希望它们之间共享同一个逻辑。

提取共享逻辑

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

自定义 Hook 是一个函数,规则以 “use” 开头,函数内部可以调用其他的 Hook

下面,是我们第一个自定义的 Hook:

// useRequest.tsx
import { useState } from "react";
import { notification } from "antd";

export const useRequest = ([service](url): Function, initData: any) => {
  const [data, setData] = useState(initData);
  const request = () => {
    service()
      .then((res: any) => {
        setData(res);
      })
      .catch(() => {
        notification.error({
          message: "网络错误",
        });
      });
  };

  const run = () => request();
  return [data, run];
};

我们来分析下做了什么? 在useRequest函数中,我们接收了service、initData作为参数,返回了data、run。 service:请求服务的promise initData: 初始化数据 data:接口返回数据 run:手动执行函数 与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头,这样可以一眼看出其符合 [Hook 的规则]。

市面上有许多优秀的库,如ahooks,但个人觉得在项目中更加偏向自己定义,因为比较灵活。

优化后的代码

  const [user, getUserInfo] = useRequest(getUserInfoApi, { name: "", age: 0 });

  return (
    <div>
      <Button onClick={getUserInfo}>获取用户信息</Button>
      <br />
      <div> name: {user.name}</div>
      <div> age: {user.age}</div>
    </div>
  );

显而易见,优化后大大减少了代码行数,但我们的useRequest并不完善。

模拟实现ahooks的useRequest部分功能

以终为始,我们先确定好useRequest需要返回什么?从而推算出需要什么参数。 返回参数:

参数说明类型
dataservice 返回的数据TData \ undefined
loadingservice 是否正在执行boolean
run手动触发 service 执行,参数会传递给 service(...params: TParams) => void
mutate直接修改 data,用法与 React.setState 一致-

接受的第二个参数Options

参数说明类型
manual默认 false 初始化自动执行。设置为 true,手动调用 run 触发执行。boolean
initData初始参数any
defaultParams首次默认执行时,传递给 service 的参数TParams
onSuccessservice resolve 时触发Function
onErrorservice reject 时触发Function
onFinallyservice 执行完成时触发Function

实现逻辑代码

// useRequest.tsx
import { useState, useEffect } from "react";
import { notification } from "antd";
import { IOptions } from "../types/useRequest";

export const useRequest = (service: Function, options?: IOptions) => {
  const { initData, manual, defaultParams, onSuccess, onError, onFinally } =
    options || {};
  const [data, setData] = useState(initData);
  const [loading, setLoading] = useState(false);

  const request = (parameter: any) => {
    setLoading(true);
    service(parameter)
      .then((res: any) => {
        setData(res);
        // 可能需要给回调传一些参数,如返回数据、参数等
        onSuccess && onSuccess(res);
      })
      .catch((err: unknown) => {
        // 可能需要给回调传一些参数,如报错信息等
        onError && onError(err);
        notification.error({
          message: "网络错误",
        });
      })
      .finally(() => {
        // 可能需要给回调传一些参数
        onFinally && onFinally();
        setLoading(true);
      });
  };

  useEffect(() => {
    // 初始化执行
    !manual && request(defaultParams);
  }, []);

  // 手动执行
  const run = (req: any) => request(req);

  // 返回参数
  return {
    loading,
    data,
    run,
    mutate: setData,
  };
};

我们来复习一下,看看我们做了些什么?\

  1. 我们将数据请求处理逻辑之间的一些共同的代码提取到单独的函数hook中。
  2. 做了一些兼容,以应对现实场景的不同情况。

最终优化后的代码:

// app.tsx
import { Button } from "antd";
import "./App.css";
import { getUserInfoApi } from "./api/user";
import { useRequest } from "./hooks/useRequest";

function App() {
  const { data: user, run: getUserInfo } = useRequest(getUserInfoApi, {
    initData: { name: "", age: 0 },
    manual: true,
  });

  return (
    <div>
      <Button onClick={getUserInfo}>获取用户信息</Button>
      <br />
      <div> name: {user.name}</div>
      <div> age: {user.age}</div>
    </div>
  );
}

export default App;

总结

我们应寻找一些方法(自定义hook),以达到简化代码逻辑,解决组件杂乱无章的目的。

参考链接