Not sure if this a React issue or not. I have an array of objects in state and am mapping each object to a component. Inside this component, I have a useEffect that sets a timeout, when the timeout completes, the object is removed from state, causing the component to be unmounted. The problem I have is, the setTimeout callback is not running until all of the current setTimeouts have expired and then they all run at the same time. I confirmed it’s not state batching by adding a console.log to the setTimeout callback and even that does not run until the last setTimeout has completed.
Here is where I map the components. removeToast is the function called to remove the object from the array in state.
<div className="toastsContainer">
{toasts.map((toast, i) => (
<Toast key={i} toast={toast} removeToast={removeToast} />
))}
</div>
removeToast function:
const removeToast = (toast: IToast) => {
setToasts((prev) => prev.filter(({ id }) => id !== toast.id));
};
Here is the Toast component with the setTimeout
import { useEffect } from "react";
import IToast from "../interfaces/toast";
import "../styles/toast.css";
import { useSpring, animated } from "react-spring";
const TOAST_DURATION = 3000;
interface ToastProps {
toast: IToast;
removeToast: (toast: IToast) => void;
}
export function Toast({ toast, removeToast }: ToastProps) {
const { width } = useSpring({
from: { width: "100%" },
to: { width: "0%" },
config: { duration: TOAST_DURATION },
});
useEffect(() => {
const timerId = setTimeout(() => {
removeToast(toast);
}, TOAST_DURATION);
return () => clearTimeout(timerId);
}, [toast, removeToast]);
return (
<div className="toast">
<div className="message"> {toast.message}</div>
<animated.div
className="duration"
style={{
width,
}}
/>
</div>
);
}
Tried having the useEffect in the parent component and mapping over the objects and setting a setTimeout there. This just results in multiple setTimeouts being applied to each object everytime an object is added.
Also tried comparing the objects directly instead of having an ID thinking it might be my filter function. This did not change any behavior.
2
Answers
The "set state function" is asynchronous. When you call that function, what it does is put a call onto the "asynch queue" to be executed "at some point in the future". When that function runs, it will queue the component that owns the state variable to be re-rendered.
React is pretty decent in terms of batching a bunch of "set state function" calls and running most/all of them prior to actually re-rendering the component that owns them.
Since it looks like all of your components are essentially being batched to run at the same timeout (+/- a couple of milliseconds), it is likely that what you are seeing is that the parent component is not rendering until all of the child components’ calls to
removeToast()
(which each callsetToasts()
) complete their runs.One experiment you might try: send the index value from the
.map()
, which is your variablei
, to each child and have them setTimeout toTOAST_DURATION * i
. Then see how the app behaves.The
useEffect
hook has theremoveToast
function as a dependency which will have a different reference on each render of the parent component resulting in the trigger of theuseEffect
hook clearing the previoussetTimeout timerId
and creating a new id on each new toast added.Suggestion:
Try Removing the
removeToast
function from the dependency array (ignoring the lint warning).Or Wrap the
removeToast
inuseCallback
hook. doing so you can safely add it to the dependency array.Also you can use useRef to track the
setTimeout timerId
instead of local variable insideuseEffect
.useEffect
hook will look like thisreact-toastify
or other libraries. I personally use it.