Search code examples
javascriptcssreactjsreact-contextreact-component

React Context with notifications list


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

Solution

  • Change your set notifications line to:

    setNotifications(prevNotifications => [...prevNotifications.filter(listNote => listNote.id !== note.id)])
    

    This will ensure you never use a stale value.