React 开发 - 自定义 Hook

448 阅读3分钟

React提供了很多的 Hook,比如 useState, useCallback 等等...本文,我们来谈谈怎么自定义 Hook

命名约定

自定义的 Hook 的名称应该是以 use 开头,以便区分普通函数和 Hook

比如 usePlayAudio.ts 就应该是一个 Hook

使用现有的 Hook

我们在自定义 Hook 中,可以使用 React 提供的内置的 Hook,比如 useState, useEffect 等。

// useMount.ts
import { useEffect, useRef } from "react";

/**
 * 组件挂载时候立即执行
 * @param fn
 */
 function useMount(fn?: () => void, clear?: () => void) {
   const unmountRef = useRef<boolean>(false);
   
   // 可以拿到 useState 的初始值
   useEffect(() => {
     unmountRef.current = false;
     fn?.();
     return () => {
       clear?.();
       unmountRef.current = true;
     }
   }, []);
   
   return {
     unmountRef,
   }
 }
 
 export default useMount;

我们使用的时候,就像平常使用自带的 Hook 那样去使用它,比如👇

// usePlayAudio.ts
import useMount from "path/to/useMount";
export function usePlayAudio() {
  useMount(() => {
    // do something
  })
}

上面,我们在自定义的 usePlayAudio 中使用自定义的 useMount

返回值

自定义的 Hook 返回值可以是任何我们需要的值。

比如返回函数👇

// useAutoScreen.ts
export const useAutoScreen = () => {
  const timeOutRef = useRef<NodeJS.Timeout | null>(null);
  
  // ...
  
  const handleCancel = () => {
    timeOutRef.current && clearTimeout(timeOutRef.current);
  }
  
  return {
    handleCancel,
  };
}

比如返回状态👇

// useStartPrint.ts
export function useStartPrint() {
    const [printNowNumber, setPrintNowNumber] = useState(0);
    const [printingImg, setPrintingImg] = useState<ReactNode>(null);
    
    // ...
    
    return {
      printNowNumber,
      printingImg
    }
}

当然,我们也可以什么都返回,在自定义的 Hook 处理了相关的逻辑。

案例

我们上面讲解了自定义 Hook 需要注意的点。下面,我们来编写一个倒计时的自定义 Hook。相关的代码如下:

// useLatestRef.ts
/**
 * 随着组件生成或者更新拿取最新的状态/函数(包括状态)
 * @param value
 * @returns ref
 */
 
 const useLatestRef = <T>(value: T): React.MutableRefObject<T> => {
  const ref = useRef(value);
  ref.current = value;
  return ref;
};

export default useLatestRef;

自定义卸载的钩子函数👇

/**
 * 卸载时以最新的函数执行
 * @param fn
 */

function useUnMount(fn: () => void) {
  const isClearRef = useRef(false);
  const ref = useLatestRef(fn);

  useEffect(
    // 这个是简写的方式
    () => () => {
      isClearRef.current = true;
      ref.current?.();
    },
    []
  );

  return isClearRef;
}
export default useUnMount;
// useCountDown.ts

// 循环设置
export function loopSetTimeout({
  isContinue,
  awaitTimeStamp,
  ref,
}: {
  sContinue: () => boolean | Promise<boolean>;
  awaitTimeStamp: number;
  ref?: {
    value: NodeJS.Timeout;
  };
}) {
  const timeOut = setTimeout(async () => {
    const bool = await isContinue();
    bool && loopSetTimeout({ isContinue, awaitTimeStamp, ref });
    clearTimeout(timeOut);
  }, awaitTimeStamp);

  if (ref) {
    ref.value = timeOut;
  }

  return timeOut;
}

export interface ICountDownParams {
  seconds: number;
  isStart?: boolean;
  isStop?: boolean;
}

// 这里 isStart 和 isStop 是非必填项,都是默认值为 true
export const useCountDown = ({ seconds, isStart = true, isStop = false }: ICountDownParams) {
  const [countDown, setCountDown] = useState(seconds);
  const isStopRef = useRef(false);
  const timeoutRef = useRef({ value: (0 as unknown) as NodeJS.Timeout });
  
  // useUnMount 是自定义的卸载的钩子函数,参考上面的 useMount 钩子函数,代码如上
  useUnMount(() => {
    timeoutRef.current.value && clearTimeout(timeoutRef.current.value)
  })
  
  const loopCountDown = () => {
    loopSetTimeout({
      isContinue: async () => {
        if (isStopRef.current) {
          return false;
        }
        // 如果没有停止,我们就对之前的计数进行减去 1
        setCountDown((prev) => {
          prev--;
          isStopRef.current = prev === 0;
          return prev;
        });
        return !isStopRef.current;
      },
      awaitTimeStamp: 1000, // 毫秒
      ref: timeoutRef.current,
    })
  }
  
  const loopCountDownRef = useLatestRef(loopCountDown);

  useEffect(() => {
    if (isStart && isStopRef.current && !isStop) {
      // 清理之前的定时器,保证不干扰
      timeoutRef.current.value && clearTimeout(timeoutRef.current.value);
      loopCountDownRef.current();
    }
    isStopRef.current = isStop;
  }, [isStop, isStart, loopCountDownRef]);

  useEffect(() => {
    if (isStart && seconds) {
      loopCountDownRef.current();
    }
  }, [isStart, seconds, loopCountDownRef]);

  const updateCoutDown = useCallback((seconds: number) => {
    setCountDown(seconds);
  }, []);
  
  // 我们返回了当前的倒计时的时间和更新倒计时的函数
  return {
    countDown,
    updateCoutDown,
  };
}

接下来,我们来使用这个钩子函数,如下:

// demo.tsx
// 引入自定义的钩子函数返回的状态或者函数
const { countDown } = useCountDown({ seconds: 30 });

return (
  <>
    <span>{ countDown }</span>
  </>
);

一引入自定义的 useCountDown 这个钩子函数,则就开始进行倒计时的功能了。这个时候,你会看到页面上 countdown 变量数值的更改。

当然,上面自定义的钩子函数,我们还可以控制更新倒计时的时间,比如下面我们在另外一个自定义的钩子函数中使用:

// useShootCountDown.ts
// 一样的,我们首先得先引入
const { countDown, updateCoutDown } = useCountDown({
  seconds: autoNextShootTime,
  isStop: stopCoutDown,
  isStart: startCountDown,
});
// 上面我们这次通过变量控制传参

useEffect(() => {
  if (enableNextShootTime) {
    updateCoutDown(autoNextShootTime);
    // 先更新倒计时
    setTimeout(() => {
      setStopCoutDown(false);
    }, 1000);
  }
}, [enableNextShootTime, autoNextShootTime, updateCoutDown]);

嗯,上面就是本文说讲的话题。【完】❀