首页 > 解决方案 > 为什么我的反应应用程序中的后续警报会显示给定会话中显示的第一个警报中的“useCountdown 挂钩计时器”?

问题描述

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

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

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

在这个错误警报的屏幕截图中,显示的每个警报都有非常不同的倒计时值,但它们都显示了第一个警报的倒计时值。倒计时在右上角。

我玩过 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

标签: javascriptreact-hooks

解决方案


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

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

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

警报容器组件:

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)使用 updater 函数而不是简单地提供一个值。在这种情况下,您可以确保您的更新是准确的,并且不会在过时的数据上完成,从而导致最后的错误状态。您可以在此处了解更多信息: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}`
}

推荐阅读