React Context with notifications list [duplicate] - javascript

This question already has answers here:
Why React useState with functional update form is needed?
(5 answers)
Closed 6 months ago.
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.

Related

React infinite scroll doesn't work after first render

I am trying to implement "infinite scroll" in my react app in which I fetch all data at once and as user scrolls down the page it displays more and more data. For that I use Intersection Observer in my custom hook with threshold of 1 to detect when user scrolls to end of "section" element so that I then can display more data. The problem is that after initial data is rendered my Intersection observer doesn't fire anymore as if it's disconnected but it's not.
Here is my custom hook:
import {useCallback, useEffect, useState} from "react";
export function useInfinityScrollObserver(ref) {
let [isVisible, setIsVisible] = useState(false);
const handleIntersection = useCallback(([entry]) => {
if (entry.isIntersecting){
setIsVisible(true)
}else if (!entry.isIntersecting){
setIsVisible(false)
}
}, [])
useEffect(() => {
const options = {
threshold: 1
}
// Create the observer, passing in the callback
const observer = new IntersectionObserver(handleIntersection, options);
// If we have a ref value, start observing it
if (ref.current) {
observer.observe(ref.current);
}
// If unmounting, disconnect the observer
return () => {
observer.unobserve(ref.current)
observer.disconnect();
}
}, [handleIntersection]);
return isVisible;
}
And here is component where I fetched data and I wanna display more data when user scrolls to the end of the "section" element:
const CountriesSection = () => {
let [data ,setData] = useState(null)
let [loadedCountries, setLoadedCountries] = useState([])
let [loadedCountriesNum, setLoadedCountriesNum] = useState(0)
const ref = useRef(null);
const isVisible = useInfinityScrollObserver(ref) // set hook to watch "section" ref
// Fetch all data at once
useEffect(() => {
async function fetchData() {
const response = await fetch(`https://disease.sh/v3/covid-19/countries?sort=cases`)
const countries = await response.json()
setData(countries)
}
fetchData()
}, [])
// When initial data is fetched or when number of Countries we wanna display changes fire this useEffect
useEffect(() => {
if (data){
const nextCountriesToShow = data.slice(loadedCountriesNum, loadedCountriesNum + 20)
.map(country => <CountryCard key={country.countryInfo._id}
country={country.country}
flag={country.countryInfo.flag}
continent={country.continent}
infected={country.cases}
recovered={country.recovered}
deaths={country.deaths} />)
setLoadedCountries(previousCountries => {
return [...previousCountries, ...nextCountriesToShow]
})
}
}, [data, loadedCountriesNum])
// When Intersection Observer in custom hook changes its state fire this effect. Which means when user scrolls down to the end of "section" element
useEffect(() => {
if (isVisible){
setLoadedCountriesNum(previousNum => {
return previousNum + 20
})
}
}, [isVisible])
return (
<section ref={ref} className={styles['section-countries']}>
{loadedCountries}
</section>
)
}
export default CountriesSection;
One more thing I noticed is when I change threshold inside of my custom hook from 1 to 0, then additional data is rendered each time when section enters my viewport.

React render component only for few seconds

In my existing react component, I need to render another react component for a specific time period.
As soon as the parent component mounts/or data loads, the new-component (or child component) should be visible after 1-2 seconds and then after another few seconds, the new-component should be hidden. This needs to be done only if there is no data available.
This is what currently I've tried to achieve:
import React, { useState, useEffect } from "react";
function App() {
const [showComponent, setShowComponent] = useState(false);
const sampleData = [];
useEffect(() => {
if (sampleData.length === 0) {
setTimeout(() => {
setShowComponent(true);
}, 1000);
}
}, [sampleData]);
useEffect(() => {
setTimeout(() => {
setShowComponent(false);
}, 4000);
}, []);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
The current implementation is not working as expected. The new-component renders in a blink fashion.
Here is the working snippet attached:
Any help to resolve this is appreciated!
Every time App renders, you create a brand new sampleData array. It may be an empty array each time, but it's a different empty array. Since it's different, the useEffect needs to rerun every time, which means that after every render, you set a timeout to go off in 1 second and show the component.
If this is just a mock array that will never change, then move it outside of App so it's only created once:
const sampleData = [];
function App() {
// ...
}
Or, you can turn it into a state value:
function App() {
const [showComponent, setShowComponent] = useState(false);
const [sampleData, setSampleData] = useState([]);
// ...
}
I have modified the code to work, hope this how you are expecting it to work.
import React, { useState, useEffect } from "react";
const sampleData = [];
// this has to be out side or passed as a prop
/*
reason: when the component render (caued when calling setShowComponent)
a new reference is created for "sampleData", this cause the useEffect run every time the component re-renders,
resulting "<h2>found error</h2>" to flicker.
*/
function App() {
const [showComponent, setShowComponent] = useState(false);
useEffect(() => {
if (sampleData.length === 0) {
const toRef = setTimeout(() => {
setShowComponent(true);
clearTimeout(toRef);
// it is good practice to clear the timeout (but I am not sure why)
}, 1000);
}
}, [sampleData]);
useEffect(() => {
if (showComponent) {
const toRef = setTimeout(() => {
setShowComponent(false);
clearTimeout(toRef);
}, 4000);
}
}, [showComponent]);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
You can try this for conditional rendering.
import { useEffect, useState } from "react";
import "./styles.css";
const LoadingComponent = () => <div>Loading...</div>;
export default function App() {
const [isLoading, setLoading] = useState(true);
const [isError, setIsError] = useState(false);
const onLoadEffect = () => {
setTimeout(() => {
setLoading(false);
}, 2000);
setTimeout(() => {
setIsError(true);
}, 10000);
};
useEffect(onLoadEffect, []);
if (isLoading) {
return <LoadingComponent />;
}
return (
<div className="App">
{isError ? (
<div style={{ color: "red" }}>Something went wrong</div>
) : (
<div>Data that you want to display</div>
)}
</div>
);
}
I needed to do imperatively control rendering an animation component and make it disappear a few seconds later. I ended up writing a very simple custom hook for this. Here's a link to a sandbox.
NOTE: this is not a full solution for the OP's exact use case. It simply abstracts a few key parts of the general problem:
Imperatively control a conditional render
Make the conditional "expire" after duration number of milliseconds.

can't clean up setTimeout in useEffect unmount cleanup

I am trying to make kind of flash message displayer to display success, error, warning messages at the top for a certain duration.
I have made the use of useRef hook to store timeouts so that I can clear it incase component unmounts before timeout completion.
Everything works as expected except, if the component unmounts before timeout callback, it does not clear the timeout which indeed is trying to setState which gives
Warning: Can't perform a React state update on an unmounted component
import React, { useEffect, useRef, useState } from 'react'
import SuccessGreen from '../../assets/SuccessGreen.svg'
import Cross from '../../assets/Cancel.svg'
import WarningExclamation from '../../assets/WarningExclamation.svg'
const ICONS_MAP = {
"warning": WarningExclamation,
"success": SuccessGreen,
"error": ""
}
export const FlashMessages = ({
duration=5000,
closeCallback,
pauseOnHover=false,
messageTheme='warning',
typoGraphy={className: 'text_body'},
firstIcon=true,
...props
}) => {
const [isDisplayable, setIsDisplayable] = useState(true)
const resumedAt = useRef(null)
const remainingDuration = useRef(duration)
const countDownTimer = useRef(null)
useEffect(() => {
countDownTimer.current = resumeDuration()
console.log(countDownTimer, "From mount")
return () => {clearTimeout(countDownTimer.current)}
}, [])
const resumeDuration = () => {
clearTimeout(countDownTimer.current)
resumedAt.current = new Date()
return setTimeout(() => forceCancel(), remainingDuration.current)
}
const pauseDuration = () => {
if(pauseOnHover){
clearTimeout(countDownTimer.current)
remainingDuration.current = remainingDuration.current - (new Date() - resumedAt.current)
}
}
const forceCancel = () => {
console.log(countDownTimer, "From force")
clearTimeout(countDownTimer.current);
setIsDisplayable(false);
closeCallback(null);
}
return isDisplayable ? (
<div onMouseEnter={pauseDuration} onMouseLeave={resumeDuration}
className={`flash_message_container ${messageTheme} ${typoGraphy.className}`} style={props.style}>
{ firstIcon ? (<img src={ICONS_MAP[messageTheme]} style={{marginRight: 8, width: 20}} />) : null }
<div style={{marginRight: 8}}>{props.children}</div>
<img src={Cross} onClick={forceCancel} style={{cursor: 'pointer', width: 20}}/>
</div>
):null
}
I have tried to mimic the core functionality of this npm package
https://github.com/danielsneijers/react-flash-message/blob/master/src/index.jsx
but whith functional component.
I think the problem is that when the mouseleave event happens, the timeout id returned by resumeDuration is not saved in countDownTimer.current, so the timeout isn't cleared in the cleanup function returned by useEffect.
You could modify resumeDuration to save the timeout id to countDownTimer.current instead of returning it:
countDownTimer.current = setTimeout(() => forceCancel(), remainingDuration.current)
and then, inside useEffect, just call resumeDuration, so the component would look like this:
import React, { useEffect, useRef, useState } from 'react'
import SuccessGreen from '../../assets/SuccessGreen.svg'
import Cross from '../../assets/Cancel.svg'
import WarningExclamation from '../../assets/WarningExclamation.svg'
const ICONS_MAP = {
"warning": WarningExclamation,
"success": SuccessGreen,
"error": ""
}
export const FlashMessages = ({
duration=5000,
closeCallback,
pauseOnHover=false,
messageTheme='warning',
typoGraphy={className: 'text_body'},
firstIcon=true,
...props
}) => {
const [isDisplayable, setIsDisplayable] = useState(true)
const resumedAt = useRef(null)
const remainingDuration = useRef(duration)
const countDownTimer = useRef(null)
useEffect(() => {
resumeDuration()
console.log(countDownTimer, "From mount")
return () => {clearTimeout(countDownTimer.current)}
}, [])
const resumeDuration = () => {
clearTimeout(countDownTimer.current)
resumedAt.current = new Date()
countDownTimer.current = setTimeout(() => forceCancel(), remainingDuration.current)
}
const pauseDuration = () => {
if(pauseOnHover){
clearTimeout(countDownTimer.current)
remainingDuration.current = remainingDuration.current - (new Date() - resumedAt.current)
}
}
const forceCancel = () => {
console.log(countDownTimer, "From force")
clearTimeout(countDownTimer.current);
setIsDisplayable(false);
closeCallback(null);
}
return isDisplayable ? (
<div onMouseEnter={pauseDuration} onMouseLeave={resumeDuration}
className={`flash_message_container ${messageTheme} ${typoGraphy.className}`} style={props.style}>
{ firstIcon ? (<img src={ICONS_MAP[messageTheme]} style={{marginRight: 8, width: 20}} />) : null }
<div style={{marginRight: 8}}>{props.children}</div>
<img src={Cross} onClick={forceCancel} style={{cursor: 'pointer', width: 20}}/>
</div>
):null
}
and it would then mimic the logic from https://github.com/danielsneijers/react-flash-message/blob/master/src/index.jsx

Why eventListener re-render React Component

I am creating a stopwatch in React.js and i am wondering why window.addEventListener('keydown', callback) re-render my component?
import { useEffect, useState } from 'react';
import './App.scss';
import Timer from './Timer';
import Button from './Button';
import Time from './Time';
const App = () => {
const [isRunning, setIsRunning] = useState(false);
const [start, setStart] = useState(new Time(0));
const [stop, setStop] = useState(new Time(0));
const handleStart = () => {
const now = new Date();
setIsRunning(true);
setStart(new Time(now));
setStop(new Time(now));
};
const handleStop = () => {
setIsRunning(false);
setStop(new Time(new Date()));
};
const getTime = () => {
if (isRunning) {
return new Time(new Date().getTime() - start.origin);
} else {
return new Time(stop.origin - start.origin);
}
};
const handleKeyDown = (key) => {
console.log(key.code === 'Space');
};
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
});
return (
<div className="stopwatch">
<Timer getTime={getTime} />
<div className="buttons">
<Button role={'start'} callback={handleStart} />
<Button role={'stop'} callback={handleStop} />
</div>
</div>
);
};
export default App;
When i click start and then stop after let's say 3s. <Timer /> show correctly time that has passed, but then when i press Space on keyboard <Timer /> is re-rendering, showing new time. Then, when i switch my web-browser to VSCode and again to web-browser, <Timer /> isn't re-rendering
Here is my Timer component
import { memo, useEffect, useRef } from 'react';
const Timer = ({ getTime }) => {
const timer = useRef();
console.log('timer rendered');
useEffect(() => {
function run() {
const time = getTime().formatted();
timer.current.textContent = `${time.m}:${time.s}.${time.ms}`;
requestAnimationFrame(run);
}
run();
return () => {
cancelAnimationFrame(run);
};
});
return <div ref={timer} className="timer"></div>;
};
export default memo(Timer);
no matter if I use [] in both or none of useEffect nothing changes.
As #davood-falahati says, adding an empty array as a second argument to useEffect would probably be desirable. From the docs:
... If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works. ...
In your use case:
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);

Can I refactor multiple useEffect to single useEffect?

I have width in state which changes with window resize and showFilters as props which changes from true to false. And I want to remove listener on unmount. So, I have used three useState for each these conditions.
So, is there any refactor I can do to use all these in single useEffect.
import React, { useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import { Icon } from 'antd'
import TrendsChart from './trendsChart'
import styled from '../styled-components'
const Chart = ({ showFilters }) => {
const [width, setWidth] = useState(null)
useEffect(() => {
window.addEventListener('resize', handleWindowResize)
updateWidth()
}, [width])
useEffect(() => {
setTimeout(updateWidth, 200)
}, [showFilters])
useEffect(() => () => {
window.removeEventListener('resize', handleWindowResize)
})
const updateWidth = () => {
const containerWidth = chartRef.current.clientWidth
setWidth(Math.floor(containerWidth))
}
const handleWindowResize = () => {
updateWidth()
}
const chartRef = useRef()
function render() {
return (
<styled.chart>
<styled.chartHeader>
Daily
</styled.chartHeader>
<styled.trendsChart id="chartRef" ref={chartRef}>
<TrendsChart width={width} showFilters={showFilters}/>
</styled.trendsChart>
<div>
<Icon type="dash" /> Credit Trend
</div>
</styled.chart>
)
}
return (
render()
)
}
Chart.propTypes = {
showFilters: PropTypes.bool.isRequired
}
export default Chart
from what i understand is
two of your useEffect can be merge
useEffect(() => {
window.addEventListener('resize',handleWindowResize)
return () => window.removeEventListener('resize',handleWindowResize)
},[width])
For the set timeout part, from what i understand. That is not needed because react will rerender everytime the width(state) is changed. Hope it is help. I'm new to react too.
You should look at CONDITIONS, when each effect works.
Listener should be installed ONCE, with cleanup:
useEffect(() => {
window.addEventListener('resize',handleWindowResize)
return () => window.removeEventListener('resize',handleWindowResize)
},[])
const handleWindowResize = () => {
const containerWidth = chartRef.current.clientWidth
setWidth(Math.floor(containerWidth))
}
NOTICE: [] as useEffect parameter, without this effect works on every render
... and it should be enough as:
handleWindowResize sets width on window size changes;
showFilters causes rerender automatically

Categories

Resources