常用Hook

2023/7/17 React

# useThrottle

节流hook。如果直接在函数组件里面给某个方法添加节流:const func = _.throttle(fn, 500),那么在组件更新时导致节流函数成为一个新的函数,内部的时间变量重置,导致节流失效。防抖同理。

import _ from 'lodash';
import { useCallback, useEffect, useRef } from 'react';

export default function useThrottle(fn: Function, delay: number, dep: any[] = []) {
  const fnRef = useRef<Function>(fn);
  const options = { leading: true, trailing: false };
  useEffect(() => {
    fnRef.current = fn;
  }, [fn]);

  return useCallback(
    _.throttle((...args) => fnRef.current(...args), delay, options),
    dep,
  );
}

// 不借助其他工具函数
/*
import { useCallback, useEffect, useRef } from 'react';

interface ThrottleOptions {
  delay: number;
  leading?: boolean; // 指定调用在节流开始前。
  trailing?: boolean; // 指定调用在节流结束后。
}

export default function useThrottle<T extends Function>(fn: T, opt: ThrottleOptions, dep: any[] = []) {
  const { delay, leading = true, trailing = true } = opt;
  const { current } = useRef<{ fn: T; timer: NodeJS.Timeout | null; startTime: number }>({
    fn,
    timer: null,
    startTime: new Date().getTime(),
  });
  const leadingRef = useRef({ leading: leading });
  useEffect(() => {
    current.fn = fn;
  }, [fn]);

  return useCallback<T>(((...args) => {
    const curTime = new Date().getTime();
    const remainTime = delay - (curTime - current.startTime);
    if (current.timer) {
      clearTimeout(current.timer);
      current.timer = null;
    }
    if (remainTime <= 0) {
      // 开始时是否执行
      if (leadingRef.current.leading) {
        current.fn.call(null, ...args);
      } else {
        leadingRef.current.leading = true;
      }
      current.startTime = new Date().getTime();
    } else if (trailing) {
      // 结束时是否执行
      current.timer = setTimeout(() => {
        current.fn.call(null, ...args);
        current.startTime = new Date().getTime();
      }, remainTime);
    }
  }) as unknown as T, dep);
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# useDebounce

import _ from 'lodash';
import { useCallback, useEffect, useRef } from 'react';

export default function useDebounce(fn: Function, delay: number, dep: any[] = []) {
  const fnRef = useRef<Function>(fn);
  const options = { leading: true };
  useEffect(() => {
    fnRef.current = fn;
  }, [fn]);

  return useCallback(
    _.debounce((...args) => fnRef.current(...args), delay, options),
    dep,
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# useWindowSize

监听窗口尺寸的变化。

import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';

interface WindowSize {
  winWidth: number;
  winHeight: number;
}

function useWindowSize(): WindowSize {
  const isAddListener = useRef(false);
  const [windowSize, setWindowSize] = useState<WindowSize>({
    winWidth: window.innerWidth,
    // winWidth: 0,
    winHeight: window.innerHeight,
    // winHeight: 0,
  });

  useEffect(() => {
    if (isAddListener.current) {
      return;
    }
    const handler = _.throttle(
      () => {
        setWindowSize({
          winWidth: window.innerWidth,
          winHeight: window.innerHeight,
        });
      },
      200,
      {
        leading: true,
        trailing: true,
      },
    );

    handler();

    window.addEventListener('resize', handler);
    isAddListener.current = true;

    // eslint-disable-next-line consistent-return
    return () => {
      window.removeEventListener('resize', handler);
    };
  }, []);

  return windowSize;
}

export default useWindowSize;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# useJudgeMobile

通过监听窗口尺寸变化,判断是否显示移动端页面。

import useWindowSize from './useWindowSize';

function useJudgeMobile() {
  const { winWidth } = useWindowSize();

  return winWidth <= 768; // 自定义尺寸
}

export default useJudgeMobile;
1
2
3
4
5
6
7
8
9

# useCountDown

倒计时。

import { useState, useEffect, useRef } from 'react';

interface CountDownReturn {
  remainTime: number;
  startCountDown: (time: number) => void;
  stopCountDown: () => void
}

interface CountDownParams {
  time?: number;
  speed?: number;
}

/**
 * @description 注意:时间单位都为秒
 * @author fengchunqi
 * @date 2023-12-21 10:44:57
 * @export
 * @param {CountDownParams} options
 */
export default function useCountDown(options: CountDownParams): CountDownReturn {
  const { time = 0, speed = 1 } = options;
  const [baseTime, setBaseTime] = useState({ time });
  const [remainTime, setRemainTime] = useState(0);
  const timer = useRef<{
    startTime: number;
    counter: number;
    timer: NodeJS.Timeout | null
  }>({
    startTime: 0,
    counter: 0,
    timer: null,
  });

  const stopTimer = () => {
    if (timer.current.timer) {
      clearTimeout(timer.current.timer);
      timer.current.timer = null;
    }
  };

  /**
   * @param time seconds
   */
  const startTimer = () => {
    const frameFunc = () => {
      const { startTime, counter } = timer.current;
      const speedMs = speed * 1000; // 转成毫秒
      const real = counter * speedMs;
      const ideal = new Date().getTime() - startTime;
      timer.current.counter++;
      const diff = ideal - real;
      const remain = baseTime.time - counter * speed;
      setRemainTime(remain);
      if (remain > 0) {
        /*
        * 程序切换后台后js暂停执行,切回页面后倒计时不继续执行,需去掉误差补偿
        * 因为实际时间很大导致diff很大,setTimeout传入的时间为负数无法执行
        */
        timer.current.timer = setTimeout(frameFunc, speedMs - diff); // 误差补偿
      } else {
        stopTimer();
      }
    };
    timer.current.timer = setTimeout(frameFunc, 0);
  };

  const resetCountDown = () => {
    stopTimer();
    timer.current = {
      startTime: new Date().getTime(),
      counter: 0,
      timer: null,
    };
    if (baseTime.time > 0) {
      startTimer();
    }
  };

  useEffect(() => {
    resetCountDown();

    return (() => {
      stopTimer();
    });
  }, [baseTime]);

  const startCountDown = (countDown: number) => {
    setBaseTime(() => ({ time: countDown }));
  };

  const stopCountDown = () => {
    stopTimer();
  };

  return {
    remainTime,
    startCountDown,
    stopCountDown,
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

# useMount

import { EffectCallback, useEffect } from 'react';

const useMount = (callback: EffectCallback) => {
  useEffect(callback, []);
};

export default useMount;
1
2
3
4
5
6
7

# useUnmount

import { useEffect, useRef } from 'react';

const useUnmount = (callback: Function) => {
  const callbackRef = useRef(callback);

  callbackRef.current = callback;

  useEffect(() => () => {
    callbackRef.current();
  }, []);
};

export default useUnmount;
1
2
3
4
5
6
7
8
9
10
11
12
13
最近更新: 2024年08月01日 17:57:25