React Hooks实现倒计时及避坑

3,975 阅读2分钟

前言

倒计时是开发过程中很常见的组件,顺其自然的我们也会按照以往的开发形式实现,然而在React Hooks中去实现这个组件的时候,很有可能你就遇到“坑”了,在不了解React Hooks的情形下,又不知从何排查问题以及了解问题的根本,因此这篇文章出现了~!

废话不多说,先码正确示例,以解兄弟姐们们的燃眉之急,之后再来根据错误示例分析问题。

nerd-4.png

正确示例1

import { useState, useEffect } from 'react';

export const CountDown = (props: any) => {
  const { countDown } = props;
  let cd: number = countDown;
  const timer: any = null;

  const [time, setTime] = useState<string>('');

  const dealData = () => {
    if (cd <= 0) {
      setTime('');
      return timer && clearTimeout(timer);
    }
    const d = parseInt(cd / (24 * 60 * 60) + '');
    const h = parseInt(((cd / (60 * 60)) % 24) + '');
    const m = parseInt(((cd / 60) % 60) + '');
    const s = parseInt((cd % 60) + '');
    setTime(`倒计时: ${d}${h}${m}${s}秒`);
    cd--;
    timer = setTimeout(() => {
      dealData();
    }, 1000);
  };

  useEffect((): any => {
    dealData();
    return () => {
        timer && clearTimeout(timer);
    }
  }, []);

  return <div className={styles.CountDown}>{time}</div>;
};

正确示例2

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

export const CountDown = (props: any) => {
  const { countDown } = props;
  const cd = useRef<number>(countDown);
  const timer = useRef<any>(null);

  const [time, setTime] = useState<string>('');

  const dealData = () => {
    if (cd.current <= 0) {
      setTime('');
      return timer.current && clearTimeout(timer.current);
    }
    const d = parseInt(cd.current / (24 * 60 * 60) + '');
    const h = parseInt(((cd.current / (60 * 60)) % 24) + '');
    const m = parseInt(((cd.current / 60) % 60) + '');
    const s = parseInt((cd.current % 60) + '');
    setTime(`倒计时: ${d}${h}${m}${s}秒`);
    cd.current--;
    timer.current = setTimeout(() => {
      dealData();
    }, 1000);
  };

  useEffect((): any => {
    dealData();
    return () => {
        timer.current && clearTimeout(timer.current);
    }
  }, []);

  return <div className={styles.CountDown}>{time}</div>;
};

错误示例

import { useState, useEffect } from 'react';

export const CountDown = (props: any) => {
  const { countDown } = props;
  const [cd, setCd] = useState<number>(countDown);
  const timer: any = null;

  const [time, setTime] = useState<string>('');

  const dealData = () => {
    if (cd <= 0) {
      setTime('');
      return timer && clearTimeout(timer);
    }
    const d = parseInt(cd / (24 * 60 * 60) + '');
    const h = parseInt(((cd / (60 * 60)) % 24) + '');
    const m = parseInt(((cd / 60) % 60) + '');
    const s = parseInt((cd % 60) + '');
    setTime(`倒计时: ${d}${h}${m}${s}秒`);
    setCd(cd--);
    timer = setTimeout(() => {
      dealData();
    }, 1000);
  };

  useEffect((): any => {
    dealData();
    return () => {
        timer && clearTimeout(timer);
    }
  }, []);

  return <div className={styles.CountDown}>{time}</div>;
};

错误示例会出现的问题

  • cduseEffect中始终是初始值;
  • DOM元素上,会出现先是初始值,然后从初始值减1,之后就不会在变化

原因

  • 因为useEffect的第二个参数是[],所以re-render(更新渲染)的时候不会重新执行effect函数,所以cduseEffect中始终是初始值;
  • 因为useEffect第一次执行的时候即初始化的时候利用setCd(cd--)重新对cd赋值,所以会触发视图重新渲染一次;
  • 如果useEffect没有第二个参数[],既没有依赖,那么就相当于didMountDidUpdate两个生命周期的合集,所以更新的时候会重新执行useEffect 【注意】:useEffect的第一个参数是一个匿名函数,匿名函数执行完会立即被销毁,所以在组件重新更新的时候,会重新调用匿名函数,并获取更改后的cd

解决方法

如上示例12,因为方式1打破了React纯函数的规则,所以更加建议方式2

【注意】

  • 在倒计时为0的时候一定要销毁倒计时
  • 在组件销毁的时候一定要手动销毁倒计时,否则即使跳转到其他页面,倒计时依旧在进行

锦鲤.png