React 自定义 Hook 开发实践

4,576 阅读4分钟

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

前言

HookReact 16.8引入的新特性,之后 Hook 被越来越多的人提及和在项目中应用,相信不少人在面试中也被问到过 Hook 的作用。

个人认为 Hook 有两个重要作用:

  1. 扩展了函数组件的功能。
  2. 复杂的状态逻辑复用变得更加简单。

而要解释这两点作用,我们需要从 Hook API 和 自定义 Hook 展开介绍。

Hook API

React 提供了许多内置 Hook API,包括:useStateuseEffectuseContextuseReduceruseCallbackuseMemouseRefuseImperativeHandleuseLayoutEffectuseDebugValue

这些 API 帮助我们在使用函数组件的情况,也能拥有 class 组件的特性,其中使用最频繁的便是 useStateuseEffect

useState 能够帮助我们使用 state 特性,在函数组件中保存状态,更新状态等:

// 保存状态
const [state, setState] = useState(initialState);
// 更新状态
setState(newState)

useEffect 能够帮助我们在函数组件渲染、更新时执行操作,类似 class 组件componentDidMountcomponentDidUpdate

// 类似componentDidMount 
useEffect(()=>{
   // do someting
},[])
// 类似componentDidUpdate
useEffect(()=>{
    // do someting
},[value])

自定义 Hook

自定义 Hook 是对内置 Hook 的包装,以创造更多更丰富的功能,接下来将会结合开发实践中使用到的一些自定义 Hook ,介绍 React Hook 对状态逻辑复用发挥的作用。

useRefs

内置 Hook useRef 返回一个可变的 ref 对象,返回的 ref 对象在组件的整个生命周期内保持不变,因此除了用来获取元素的 ref 访问 DOM 元素外,还可以用于存储变量

useRefs 是对 useRef 内置 Hook 功能的扩展,当我们需要在组件内同时获取到多个元素的 ref 时,useRefs 能够用更简洁的代码达到目的。

function useRefs<RefType>(): [
  (key: React.Key) => React.RefObject<RefType>,
  (key: React.Key) => void
] {
  // 通过 useRef 创建的一个 ref 对象存储一个 Map 类型的变量 cacheRefs
  const cacheRefs = useRef(new Map<React.Key, React.RefObject<RefType>>());
  // get 方法通过key值从 cacheRefs 中获取 ref 对象或创建一个新的 ref 对象
  function getRef(key: React.Key) {
    if (!cacheRefs.current.has(key)) {
      cacheRefs.current.set(key, React.createRef());
    }
    return cacheRefs.current.get(key);
  }
  // remove 方法根据 key 值从 cacheRefs 中移除对应的 ref 对象
  function removeRef(key: React.Key) {
    cacheRefs.current.delete(key);
  }
  // 将 get 和 remove 方法作为自定义 hook 的返回值,
  return [getRef, removeRef];
}

在函数组件中复用这个自定义 Hook:

  const [getRefs,removeRefs] = useRefs<HTMLElement>();
  ...
  <>
  <div ref={getRefs('div_1')}></div>
  <div ref={getRefs('div_2')}></div>
  </>

useRequest

useRequest 是对 Http 请求的封装,通过与内置 Hook useState 的结合,让我们采用 Hook 的形式调用 Http 请求,并且获取到返回的数据时自动更新组件,而不需要额外定义状态:

function useRequest(options) {
  // http 请求相关的配置
  const { url, ...init } = options;
  // 请求返回的数据
  const [data, setData] = useState(null);
  // 请求返回的错误信息
  const [error, setError] = useState(null);
  // 请求的loading 状态
  const [loading, setLoading] = useState(false);

  // loader 方法通过 fetch API 发出 http 请求
  function loader() {
    setLoading(true);
    return fetch(url, init)
      .then((res) => {
        setData(res.json());
        setLoading(false);
      })
      .catch((error) => {
        setError(error);
        setLoading(false);
      });
  }
  // 将 loader, data, error, loading 作为自定义 hook 的返回值
  return { loader, data, error, loading };
}

在函数组件内复用这个自定义Hook:

   const {data,loader} = useRequest({url: 'api/getData', method: 'GET'})
   ...
   
   <Button onClick={()=>{loader()}}>request<Button>
   ...
   <div>{data}</div>
   

useCheckPermission

useCheckPermission 中封装了项目中校验操作权限的状态逻辑。

通过自定义 Hook 的形式,能够将这部分的状态逻辑从组件中解耦合,并且能够在不同的组件中需要校验权限的方法复用这块逻辑

通过将这部分状态逻辑抽离,也使得组件内的的代码更加清晰简洁

这个自定义 Hook 中主要结合了内置 Hook useEffect,以及自定义 Hook useRequest

function useCheckPermission() {
    // 请求验证权限
  const { loader, data } = useRequest({url: '/getPermission'});

    // 组件渲染完成时,触发校验请求
  useEffect(() => {
      loader()
  }, []);
    // 返回校验结果
  return {
    hasPermission: data,
  };
}

useTicker

useTickerHook 的方式实现定时器的功能。

通过使用内置 Hook useRefuseEffect, 能够在组件渲染完成后自动开始执行定时器,并且允许设置 callback 函数和时间间隔 time

callbacktime 更新时,能够更新定时器,设置新的间隔时间 time 和执行新的 callback


function useTicker({ callback, time }) {
  // 通过 useRef 设置 timer 变量,组件的整个生命周期内保持不变
  const cacheRef = useRef({
    timer: null,
  });

  // 设置定时器,将定时器保存在变量 cacheRef 中。
  function ticker() {
    cacheRef.current.timer = setTimeout(() => {
      callback();
      ticker();
    }, time);
  }
  // 停止定时器
  function stop() {
    clearTimeout(cacheRef.current.timer);
    cacheRef.current.timer = null;
  }
  // 当 callback 和 time 更新时,清除旧的定时器,设置新的定时器
  useEffect(() => {
    stop();
    ticker();
  }, [callback, time]);

  // 组件渲染完成,开启定时器,组件卸载时,清除定时器
  useEffect(() => {
    ticker();
    // 组件卸载时执行
    return () => {
      stop();
    };
  }, []);
  // 将 stop 方法作为 hook 的返回值
  return {
    stop,
  };
}

在组件内使用 useTicker:

    const {stop} = useTicker({callback: doSometing, time: 1000})
    function doSometing(){... }

总结

Hook 作为 React 的新特性,确实为我们提供了更多的可能性。通过自定义 Hook 的方式,我们可以将组件中可复用的状态逻辑抽离出来,保持其独立性。而当我们需要它时又可以很容易的将这些状态逻辑重新应用到组件内,与组件紧密的结合在一起,发挥它的作用。