为啥我的反应应用程序中的后续警报会显示给定会话中显示的第一个警报中的“useCountdown 挂钩计时器”?

Posted

技术标签:

【中文标题】为啥我的反应应用程序中的后续警报会显示给定会话中显示的第一个警报中的“useCountdown 挂钩计时器”?【英文标题】:Why do subsequent Alerts in my react app show the "useCountdown hook timer" from the first Alert shown in a given session?为什么我的反应应用程序中的后续警报会显示给定会话中显示的第一个警报中的“useCountdown 挂钩计时器”? 【发布时间】:2020-12-20 14:16:21 【问题描述】:

我的橡皮鸭没有给我答案。他通常比较聪明。

在我的 Create-React-App 中,我有一个警报上下文和一个反应组件,用于处理整个应用程序中的弹出警报。我第一次使用 React Hooks 并尝试在每个警报上设置一个倒计时。

我编写了一个 useCountdown 钩子(代码如下所示),它在其他测试组件上运行良好,但是当我尝试将它与我的警报一起使用时,给定会话中的每个警报都使用触发的第一个警报中的计时器值。

在这个screen shot of faulty alerts 中,显示的每个警报都有非常不同的倒计时值,但它们都显示了第一个警报的倒计时值。倒计时在右上角。

我曾尝试使用 useEffect() 来尝试解决此问题,但很明显我还有一些关于钩子及其作用域的知识要学习。

任何建议将不胜感激。最终,我会将计时器设置为一个小的手动关闭按钮,其中秒数会显示为按钮的文本,直到自动关闭。目前...只是处于丑陋模式。

使用倒计时挂钩

import  useState, useEffect  from 'react'

const useCountdown = (m = 1, s = 10) => 
  const [minutes, setMinutes] = useState(m)
  const [seconds, setSeconds] = useState(s)

  useEffect(() => 
    let myInterval = setInterval(() => 
      if (seconds > 0) 
        setSeconds(seconds - 1)
      
      if (seconds === 0) 
        if (minutes === 0) 
          clearInterval(myInterval)
         else 
          setMinutes(minutes - 1)
          setSeconds(59)
        
      
    , 1000)
    return () => 
      clearInterval(myInterval)
    
  )

  const finalSeconds = seconds < 10 ? `0$seconds` : `$seconds`

  return `$minutes:$finalSeconds`


export default useCountdown

警报组件

import React,  useContext, Fragment  from 'react'
import AlertContext from '../../context/alert/alertContext'
import  CSSTransition, TransitionGroup  from 'react-transition-group'
import styled from 'styled-components' // TODO Refactor to styled

// Custom hook to give each Alert a countdown timer
import useCountdown from '../../hooks/useCountdown' // BUG!

const Alerts = () => 
  // const testAlert = 
  //   id: 12345,
  //   title: 'Problem',
  //   color: 'alert-danger',
  //   icon: 'fas fa-exclamation-triangle',
  //   seconds: 20,
  //   msg: ['This is an alert. Testing 123.', 'Auto-dismiss in 20 seconds.'],
  // 

  const alertContext = useContext(AlertContext)
  const  removeAlert  = alertContext

  // ISSUE #32 The initial value gets used through-out session
  const countdown = useCountdown(alert.seconds)

  return (
    <Fragment>
      <TransitionGroup>
        alertContext.alerts.length > 0 &&
          alertContext.alerts.map((alert) => (
            <CSSTransition key=alert.id timeout=500 classNames='pop'>
              <div className=`alert $alert.color`>
                <div className='alert-title'>
                  <h1 onClick=() => removeAlert(alert.id)>
                    <i className=`$alert.icon` /> <span className='hide-sm'>alert.title</span>
                  </h1>
                  <span className='alert-countdown'>countdown</span>
                  <button className='btn btn-link btn-sm alert-btn hide-sm' onClick=() => removeAlert(alert.id)>
                    <i className='far fa-times-square fa-3x' />
                  </button>
                </div>
                <div className='alert-items'>
                  <ul>
                    alert.msg.map((item) => (
                      <li key=item>
                        <i className='fas fa-chevron-circle-right'></i> <span>item</span>
                      </li>
                    ))
                  </ul>
                </div>
              </div>
            </CSSTransition>
          ))
      </TransitionGroup>
    </Fragment>
  )


export default Alerts

【问题讨论】:

【参考方案1】:

问题是您在Alerts 容器 组件中使用了useCountdown,因此为所有子元素存储了一个单一 状态。基本上,目前您只有一个时间间隔,因此有一个倒计时值可用于所有警报。

如果您希望每个警报都有一个单独的状态(倒计时值),您应该将警报 UI 提取到一个单独的组件中,并在那里使用 useCountdown 挂钩。然后,重构 Alerts 容器组件,为每个实例呈现一个 Alert 组件。

这是一个示例代码。它可能包含语法错误,因为我现在无法对其进行测试,但请尝试一下:

警报容器组件:

import React,  useContext, Fragment  from 'react'
import AlertContext from '../../context/alert/alertContext'
import  CSSTransition, TransitionGroup  from 'react-transition-group'
import styled from 'styled-components' // TODO Refactor to styled

const Alerts = () => 
  const alertContext = useContext(AlertContext)
  const  removeAlert  = alertContext

  return (
    <Fragment>
      <TransitionGroup>
        alertContext.alerts.length > 0 &&
          alertContext.alerts.map((alertInstance) => (
              <CSSTransition key=alertInstance.id timeout=500 classNames='pop'>
                <Alert alert=alertInstance removeAlert=removeAlert/>
              </CSSTransition>
          ))
      </TransitionGroup>
    </Fragment>
  )


export default Alerts

警报组件:

export function Alert(props) 
    const countdown = useCountdown(props.alert.seconds);

    return (        
        <div className=`alert $props.alert.color`>
            <div className='alert-title'>
                <h1 onClick=() => props.removeAlert(props.alert.id)>
                    <i className=`$props.alert.icon` /> <span className='hide-sm'>props.alert.title</span>
                </h1>
                <span className='alert-countdown'>countdown</span>
                <button className='btn btn-link btn-sm alert-btn hide-sm' onClick=() => props.removeAlert(props.alert.id)>
                    <i className='far fa-times-square fa-3x' />
                </button>
            </div>
            <div className='alert-items'>
                <ul>
                    props.alert.msg.map((item) => (
                        <li key=item>
                            <i className='fas fa-chevron-circle-right'></i> <span>item</span>
                        </li>
                    ))
                </ul>
            </div>
        </div>        
    );

另外,当改变依赖于先前值的状态时,如setMinutes(minutes - 1) 中使用更新函数而不是简单地提供一个值。在这种情况下,您确保您的更新将是准确的,并且不会在过时的数据上完成,从而导致最后的错误状态。你可以在这里了解更多信息:useState Reference。

所以你的钩子应该是这样的:

const useCountdown = (m = 1, s = 10) => 
  const [minutes, setMinutes] = useState(m)
  const [seconds, setSeconds] = useState(s)

  useEffect(() => 
    let myInterval = setInterval(() => 
      if (seconds > 0) 
        setSeconds((seconds) => seconds - 1)
      
      if (seconds === 0) 
        if (minutes === 0) 
          clearInterval(myInterval)
         else 
          setMinutes((minutes) => minutes - 1)
          setSeconds(59)
        
      
    , 1000)
    return () => 
      clearInterval(myInterval)
    
  )

  const finalSeconds = seconds < 10 ? `0$seconds` : `$seconds`

  return `$minutes:$finalSeconds`

【讨论】:

这让我走上了正轨。谢谢你非常详细的回答。将单个警报从阵列中拉出并将它们发送到第二个组件以处理单个警报的重构是解决方法。 另外,关于正确使用 useState 更新程序函数和函数的提示,这样我就不会因过时状态而绊倒,非常受欢迎。我在学习钩子的过程中学到了这一点,但显然已经忘记了。再次感谢您的完整回答。 setMinutes((minutes) => minutes - 1) 而不是 setMinutes(minutes - 1)

以上是关于为啥我的反应应用程序中的后续警报会显示给定会话中显示的第一个警报中的“useCountdown 挂钩计时器”?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 iOS 会显示警告“此应用程序需要由开发人员更新才能在此版本的 iOS 上运行”。对于我的反应原生应用程序?

从连续反应中读取会话存储的变化

如何在 ios 日历中的 EKEvent 中设置特定警报?

为啥 Safari 3.1 不显示我的警报

发出警报后,我无法在我的反应本机应用程序中关闭我的键盘

在 JSF 2 的 requestScoped bean 中显式创建会话