You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Context管理通知列表:批量关闭时通知复现问题求助

问题描述

我使用React Context存储所有通知的列表。创建通知时,将Context状态设置为添加新通知后的新列表,触发重渲染并在页面上渲染所有通知;当通知到期时,更新Context过滤掉该通知。

不过通知带有滑出动画,需要等动画完成后再更新通知列表。但批量关闭多个通知时,部分通知会重新出现,原因是它们同时使用旧列表进行状态更新。


Notification Context 代码

import { createContext, useContext, useState } from "react"
import Notification from "../components/Notification"

const NotificationsContext = createContext()
const SetNotificationsContext = createContext()

export function useNotifications() {
    return useContext(NotificationsContext)
}

export function useSetNotifications() {
    return useContext(SetNotificationsContext)
}

export default function NotificationsProvider({children}) {
    const [notifications, setNotifications] = useState([])

    return (
        <NotificationsContext.Provider value={notifications}>
            <SetNotificationsContext.Provider value={setNotifications}>
                <div className="notifications-wrapper">
                    {notifications.map(note => {
                        return <Notification key={note.id} note={note}/>
                    })}
                </div>
                {children}
            </SetNotificationsContext.Provider> 
        </NotificationsContext.Provider>
    )
}

Notification Component 代码

import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useNotifications, useSetNotifications } from '../contexts/NotificationsProvider'
import useInterval from '../hooks/useInterval'

function Notification({note}) {
    const noteRef = useRef()

    const notifications = useNotifications()
    const setNotifications = useSetNotifications()

    const [width, setWidth] = useState(0)
    const [delay, setDelay] = useState(null)

    // 增长进度条
    useInterval(
        useCallback(
            () => setWidth(oldWidth => oldWidth + 1), 
            [setWidth]
        ), 
    delay)

    // 滑入动画(左滑)
    useEffect(() => {
        noteRef.current.onanimationend = () => {
            noteRef.current.classList.remove("slide-left")
            noteRef.current.onanimationend = undefined
        }

        noteRef.current.classList.add("slide-left")
    }, [])

    const handleStartTimer = useCallback(() => {
        console.log("timer STARTED")
        setDelay(10)
    }, [setDelay])

    const handlePauseTimer = useCallback(() => {
        console.log("timer PAUSED")
        setDelay(null)
    }, [setDelay])

    // 关闭通知并触发滑出动画(右滑)
    const handleCloseNotification = useCallback(() => {
        console.log("slided right / removed notification")
        handlePauseTimer()
        noteRef.current.onanimationend = () => {
            setNotifications([...notifications.filter(listNote => listNote.id !== note.id)])
        }
        noteRef.current.classList.add("slide-right")
    }, [note.id, notifications, setNotifications, handlePauseTimer])

    // 通知到期自动关闭
    useLayoutEffect(() => {
        if (width >= noteRef.current.clientWidth - 10) {
            handleCloseNotification()
        }
    }, [width, handleCloseNotification, handlePauseTimer])

    // 通知创建时启动计时器
    useEffect(() => {
        handleStartTimer()

        return handlePauseTimer
    }, [handleStartTimer, handlePauseTimer])
    
    return (
        <div ref={noteRef} className={`notification-item ${note.type === "SUCCESS" ? "success" : "error"}`}
        onMouseOver={handlePauseTimer} onMouseLeave={handleStartTimer}>
            <button onClick={handleCloseNotification} className='closing-button'>✕</button>
            <strong>{note.text}</strong>
            <div className='notification-timer' style={{"width":`${width}px`}}></div>
        </div>
    )
}

export default Notification

问题原因与修复方案

问题根源

批量关闭通知时,每个Notification组件在执行handleCloseNotification时,依赖的是组件挂载或上次更新时捕获的notifications旧状态。当多个组件同时触发更新,它们都会基于同一个旧状态生成新列表,导致后执行的更新会覆盖先执行的,最终只有最后一个关闭的操作生效,其他通知会重新出现。

修复方案

使用React状态更新的函数式更新,让setNotifications接收一个回调函数,该函数会传入当前最新的状态,基于这个最新状态来过滤列表,避免状态覆盖。

修改handleCloseNotification中的状态更新逻辑:

// 原代码
setNotifications([...notifications.filter(listNote => listNote.id !== note.id)])

// 修改为函数式更新
setNotifications(prevNotifications => prevNotifications.filter(listNote => listNote.id !== note.id))

同时,需要将notifications从useCallback的依赖数组中移除,因为现在不再直接依赖它:

const handleCloseNotification = useCallback(() => {
    console.log("slided right / removed notification")
    handlePauseTimer()
    noteRef.current.onanimationend = () => {
        setNotifications(prevNotifications => prevNotifications.filter(listNote => listNote.id !== note.id))
    }
    noteRef.current.classList.add("slide-right")
}, [note.id, setNotifications, handlePauseTimer]) // 移除了notifications依赖

这样每次更新都会基于最新的通知列表进行过滤,批量关闭时就不会出现通知重新出现的问题了。


内容的提问来源于stack exchange,提问作者Joao Barrera

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 21:35:21