I am using a context to hold a list of all my notifications. When a notification is created, the context's state is set to a new list with this new notification added. The app re-renders, loops, and renders all the notifications to the screen. When a notification is due, the context is updated to filter out that notification.
However, the notifications have an animation so they can slide to the right before they are removed, which I made so it yields to update the notifications list until the animation is done. Unfortunately, if I close multiple notifications at once, some of them comeback because they are all simultaneously trying to update with an old list.
I don't know where to go.
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) // grow progress bar useInterval( useCallback( () => setWidth(oldWidth => oldWidth + 1), [setWidth] ), delay) // sliding left animation useEffect(() => { // console.log("slided left") 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]) // filter off notification and sliding right animation 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]) // notification is due useLayoutEffect(() => { if (width >= noteRef.current.clientWidth - 10) { handleCloseNotification() } }, [width, handleCloseNotification, handlePauseTimer]) // start timer when notification is created 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
Change your set notifications line to:
setNotifications(prevNotifications => [...prevNotifications.filter(listNote => listNote.id !== note.id)])
This will ensure you never use a stale value.