如何防止复杂的状态变化导致重复的 setTimeout?

How can one prevent duplicate setTimeout resulting from complex state change?

提问人:cyclingLinguist 提问时间:1/24/2023 最后编辑:cyclingLinguist 更新时间:1/25/2023 访问量:95

问:

对于这个问题所基于的代码的复杂性,我深表歉意,但似乎问题本身是由复杂性引起的。我无法用一个更简单的例子来复制这个问题。这是有问题的代码存储库分支: https://github.com/chingu-voyages/v42-geckos-team-21/commit/3c20cc55e66e7d0f9122d843222980e404d4910f 左手(更改前)使用 useRef() 并且没有问题,但我认为 useRef() 不尊重它的正确用法。

以下是主要问题代码:

import { useState, useRef} from 'react';
import "./Alert.css"
import "animate.css"
import { CSSTransition } from "react-transition-group"
import { time } from 'console';


console.log(typeof CSSTransition);

interface IfcProps {
  text: React.ReactNode,
  exitAfterDuration: number,
  setAlertKey: React.Dispatch<React.SetStateAction<number>>,
  alertKey: number
}

const classNames = {
  appear: 'animate__bounce',
  appearActive: 'animate__bounce',
  appearDone: 'animate__bounce',
  enter: 'animate__bounce',
  enterActive: 'animate__bounce',
  enterDone: 'animate__bounce',
  exit: 'animate__bounce',
  exitActive: 'animate__fadeOut',
  exitDone: 'animate__fadeOut'
}

function Alert(props: IfcProps) {

  const nodeRef = useRef(null);
  

  let [isIn, setIsIn] = useState(false);
  let [previousAlertKey, setPreviousAlertKey] = useState(0);
  let [timeoutId, setTimeoutId] = useState<number | null>(null);


  // console.log('props', {...props});
  console.log('prev, pres', previousAlertKey, props.alertKey)
  console.log('state', {isIn, previousAlertKey, timeoutId});

  // console.log('prev, current:', previousAlertKey, props.alertKey);
  if (props.text === '') {
    // do not render if props.text === ''
    return null;
  } else if (previousAlertKey !== props.alertKey) {
    
    setIsIn(true);
    setPreviousAlertKey(oldPreviousAlertKey => {
      
      oldPreviousAlertKey++
      return oldPreviousAlertKey;
    });


    if (timeoutId) {
      console.log(timeoutId, 'timeout cleared');
      clearTimeout(timeoutId);
    }

    let localTimeoutId = window.setTimeout(() => {
      console.log('executing timeout')
      setIsIn(false);
    }, props.exitAfterDuration);
   
    console.log({localTimeoutId}, previousAlertKey, props.alertKey);
    setTimeoutId(localTimeoutId);


  }
  
 
      


  

  return (
    <CSSTransition nodeRef={nodeRef} in={isIn} appear={true} timeout={1000} classNames={classNames}>
      {/* Using key here to trigger rebounce on alertKey change */}
      <div ref={nodeRef} id="alert" className="animate__animated animate__bounce" key={props.alertKey}>
        {props.text}
      </div>
    </CSSTransition>
  )
}

export default Alert

解决了问题但可能不正确地使用了 useRef() 的代码:

import { useState, useRef } from 'react';
import "./Alert.css"
import "animate.css"
import { CSSTransition } from "react-transition-group"
import { time } from 'console';


console.log(typeof CSSTransition);

interface IfcProps {
  text: React.ReactNode,
  exitAfterDuration: number,
  setAlertKey: React.Dispatch<React.SetStateAction<number>>,
  alertKey: number
}

const classNames = {
  appear: 'animate__bounce',
  appearActive: 'animate__bounce',
  appearDone: 'animate__bounce',
  enter: 'animate__bounce',
  enterActive: 'animate__bounce',
  enterDone: 'animate__bounce',
  exit: 'animate__bounce',
  exitActive: 'animate__fadeOut',
  exitDone: 'animate__fadeOut'
}

function Alert(props: IfcProps) {

  const nodeRef = useRef(null);
  const timeoutIdRef = useRef<number | null>(null);

  let [isIn, setIsIn] = useState(false);
  let [previousAlertKey, setPreviousAlertKey] = useState(0);

  console.log({props});
  console.log('state', {isIn, previousAlertKey, timeoutIdRef});

  console.log('prev, current:', previousAlertKey, props.alertKey);
  if (props.text === '') {
    // do not render if props.text === ''
    return null;
  } else if (previousAlertKey !== props.alertKey) {
    
    setIsIn(true);
    setPreviousAlertKey(oldPreviousAlertKey => {
      
      oldPreviousAlertKey++
      return oldPreviousAlertKey;
    });


    if (timeoutIdRef.current) {
      console.log(timeoutIdRef.current, 'timeout cleared');
      clearTimeout(timeoutIdRef.current);
    }

    let localTimeoutId = window.setTimeout(() => setIsIn(false), props.exitAfterDuration);
   
    console.log({localTimeoutId}, previousAlertKey, props.alertKey);
    timeoutIdRef.current = localTimeoutId;
  }
  
      


  

  return (
    <CSSTransition nodeRef={nodeRef} in={isIn} appear={true} timeout={1000} classNames={classNames}>
      {/* Using key here to trigger rebounce on alertKey change */}
      <div ref={nodeRef} id="alert" className="animate__animated animate__bounce" key={props.alertKey}>
        {props.text}
      </div>
    </CSSTransition>
  )
}

export default Alert

当尝试将无效行提交到数据库并出现警报组件时,问题会显示其头。如果以这种方式触发多个警报,则当第一个 setTimeout 过期时,它们都会消失,因为它从未被正确清除。应该清除一个超时,但由于 React 严格模式渲染两次,并且创建超时是一个副作用,因此额外的超时永远不会被清除。React 不知道每次提交尝试(复选标记单击)都会运行两次超时。

我可能错误地处理了我的警报组件,例如alertKey。

我觉得我的问题与以下事实有关:我的 setTimeout 是在 Alert 组件内部触发的,而不是在 Row 组件的 onClick() 处理程序中触发的,因为我在一个更简单的示例中这样做了,它没有表现出这个问题。

我担心我可能不会得到任何回复,因为这是一个非常丑陋和复杂的案例,需要对开发环境进行相当多的设置。在这种情况下,我只需要拼凑一个解决方案(例如使用 useRef),并通过经验在未来学习正确的 React 方式。隧道视觉是我的缺点之一。

reactjs settimeout setstate 严格模式

评论

1赞 Azzy 1/24/2023
我认为您应该考虑使用带有清理的 useEffect 来清除计时器,我在这里进行了解释
0赞 cyclingLinguist 1/24/2023
@Azzy我实际上用useEffect尝试了类似的东西,但问题没有解决,但很可能我没有正确实现它。我会再试一次!
0赞 0stone0 1/24/2023
请不要发布指向 github 的链接,请将您的相关代码放在这里
0赞 cyclingLinguist 1/24/2023
@0stone0我更新了问题,但是否可以仍然包含 Github 链接,因为我相信该问题可能包括代码的其他部分?

答:

0赞 cyclingLinguist 1/25/2023 #1

tl;dr 在 中使用依赖数组useHook()

因此,我退后一步,研究了应用程序的其他一些部分,同时有时对其他人如何处理 Toast 通知组件进行一些研究,这就是我在这里的代码中有效地工作的内容。Logrocket 的文章很有帮助:如何使用 React 创建自定义 toast 组件

@Azzy帮助我回到了正确的轨道上,上面的文章也使用了 useEffect() 钩子来超时。

在业余时间,我最终看到了这篇文章 React.useEffect() 的简单解释。作者 Dmitri Pavlutin 终于在我的脑海中了解了主要组件函数体和 useEffect 钩子的预期关系。

一个函数式 React 组件使用 props 和/或 state 来计算 输出。如果功能组件进行的计算没有 以输出值为目标,然后将这些计算命名为 副作用。

. . .

组件渲染和副作用逻辑是独立的。它 直接在身体中执行副作用将是一个错误 组件,主要用于计算输出。

组件渲染的频率不是您可以控制的 — 如果 React 想要渲染组件,你无法阻止它。

. . .

如何将渲染与副作用解耦?欢迎使用Effect() — 独立于渲染运行副作用的钩子。

我现在介绍的是有效的代码,并且是 React 方式(或者至少比我之前的两次尝试更像是最佳的 React 方式,见上文)。

function Alert(props: IfcProps) {
  useEffect(() => {
    let timeoutId = window.setTimeout(() => setIsIn(false), props.exitAfterDuration);

    return function cleanup() {
      window.clearTimeout(timeoutId);
    }

  }, [props.alertKey]);



  const nodeRef = useRef(null);
  const timeoutIdRef = useRef<number | null>(null);

  let [isIn, setIsIn] = useState(false);
  let [previousAlertKey, setPreviousAlertKey] = useState(0);

  console.log({ props });
  console.log('state', { isIn, previousAlertKey, timeoutIdRef });

  console.log('prev, current:', previousAlertKey, props.alertKey);
  if (props.text === '') {
    // do not render if props.text === ''
    return null;
  } else if (previousAlertKey !== props.alertKey) {

    setIsIn(true);
    setPreviousAlertKey(oldPreviousAlertKey => {

      oldPreviousAlertKey++
      return oldPreviousAlertKey;
    });
  }






  return (
    <CSSTransition nodeRef={nodeRef} in={isIn} appear={true} timeout={1000} classNames={classNames}>
      {/* Using key here to trigger rebounce on alertKey change */}
      <div ref={nodeRef} id="alert" className="animate__animated animate__bounce" key={props.alertKey}>
        {props.text}
      </div>
    </CSSTransition>
  )
}

export default Alert

老实说,现在我可能有很多重构,因为我了解了 .我可能还有其他组件的副作用取决于专门用于检查当前渲染是否因为特定状态/道具更改而发生的逻辑。的依赖数组比函数体中的条件数组要干净得多,用于检查这些状态/属性更改。useEffect()useEffect()

我认为,我在问题中感叹的很多复杂性都是因为我没有正确地将副作用与我的主要功能体分开。

谢谢你来参加我的TED演讲。:)