React Hook设计,如何更轻松使用Interval与Timeout

258 阅读3分钟

背景

    场景需要动态开启轮询,根据场景停止或启动。直接使用 setInterval 并不是特别方便,还要在组件卸载/重新render时清理定时器。于是考虑将 setInterval 封装为 hooks ,简化使用。

功能设计

    期望它是一个功能函数,接受参数为 fn 回调,返回值为触发函数 run 和定时器清理函数 clear

功能实现

    同样的,首先考虑入参, fn 回调与 delay 时间间隔是首要参数。为了方便使用,同时不对 ahooks 库的 useInterval 产生混淆。这里同样对它支持 option 参数。

export interface UseCreateIntervalProps {
  fn: () => void, 
  delay: number;
  options?:  {
    immediate?: boolean;
  };
}

    在 hook 设计过程中,考虑到 run 函数可能会重复调用,但是每次 run 都会产生一个定时器,这是不可接受的。于是在 options 中增加参数 autoClear,在每次调用 run 时清理之前废弃定时器。好的,废话不多说,下面贴代码。

import { useMemoizedFn } from 'ahooks';
import { isNumber } from 'lodash';
import { useCallback,  useRef } from 'react';

export interface UseCreateIntervalOptions {
  immediate?: boolean;
  autoClear?: boolean;
}

export interface UseCreateIntervalProps {
  fn: () => void, 
  delay?: number;
  options?: UseCreateIntervalOptions;
}

export const useCreateInterval = ({
  fn,
  delay = 1000,
  options = {
    immediate: true,
    autoClear: true,
  },
}: UseCreateIntervalProps) => {
  const timerCallback = useMemoizedFn(fn);
  const timerRef = useRef<NodeJS.Timeout>();

  const clear = useCallback(() => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
  }, []);

  const run = useCallback(() => {
    if (!isNumber(delay) || delay < 0) {
      return;
    }

    if (options?.autoClear) {
      clear();
    }

    if (options?.immediate) {
      timerCallback();
    }

    timerRef.current = setInterval(timerCallback, delay);

    return clear;
  }, [delay, options]);

  return { 
    run,
    clear,
  };
};

export default useCreateInterval;

使用示例

    上面封装完成,有点工厂函数的感觉,需要先通过 useCreateInterval 得到我们的目标函数,然后再进行调用。示例如下。

const test = (fn: () => void) => {
  const { run, clear } = useCreateInterval({ fn, delay: 5000 });

  useEffect(() => run(), []);
}

扩展 useCreateInterval

    上面已经能够很灵活且方便地使用 interval 了,但我就是想用类似 effefct 的形式调用,连 useEffect 我都不想写怎么办呢?这里考虑增加参数 active, 通过控制 active 来控制定时器的启动或停止。实现如下。


export interface UseEffectIntervalProps extends UseCreateIntervalProps {
  active: boolean;
}

export const useEffectInterval = ({ active, ...restProps }: UseEffectIntervalProps) => {
  const { run, clear } = useCreateInterval(restProps);

  useEffect(() => active ? run() : clear(), [active]);
};

export default useEffectInterval;

举一反三

    根据上面的设计,可以很轻松的复刻出 setTimeout 的封装。根据他俩的实际应用场景,只需要细微的更改即可。下面是代码实例。

import { useMemoizedFn } from 'ahooks';
import { isNumber } from 'lodash';
import { useCallback, useRef } from 'react';

export interface UseCreateTimeoutOptions {
  autoClear?: boolean;
}

export interface UseCreateTimeoutProps {
  fn: () => void,
  delay?: number;
  options?: UseCreateTimeoutOptions;
}

export const useCreateTimeout = ({
  fn,
  delay = 1000,
  options = {
    autoClear: true,
  },
}: UseCreateTimeoutProps) => {
  const timerCallback = useMemoizedFn(fn);
  const timerRef = useRef<NodeJS.Timeout>();

  const clear = useCallback(() => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
  }, []);

  const run = useCallback(() => {
    if (!isNumber(delay) || delay < 0) {
      return;
    }

    if (options?.autoClear) {
      clear();
    }

    timerRef.current = setTimeout(timerCallback, delay);

    return clear;
  }, [delay, options]);

  return {
    run,
    clear,
  };
};

export default useCreateTimeout;

扩展 useCreateTimeout

    根据 setTimeout 的特点,当启用 timeout 定时器时,它是在指定时间后执行回调。推断我们业务中更多是按需启动定时器,于是这里 active 参数定义为 any, 当它产生变化时即可启用。示例如下。

import { useEffect } from 'react';

import { UseCreateTimeoutProps, useCreateTimeout } from './useCreateTimeout';

export interface UseEffectTimeoutProps extends UseCreateTimeoutProps {
  active: any;
}

export const useEffectTimeout = ({ active, ...restProps }: UseEffectTimeoutProps) => {
  const { run, clear } = useCreateTimeout(restProps);

  useEffect(() => {
    return active ? run() : clear();
  }, [active]);
};

export default useEffectTimeout;