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
相关产品推荐
相关产品推荐

