I reused my alert message system I wrote in a an older version of React (maybe 16?) and that was working fine, but in this new project (using React 18) it doesn’t work and I don’t understand why.
I created a CodeSandbox to illustrate the issue: https://codesandbox.io/p/sandbox/react-alert-system-q5rwtn
Here is the main file app.js
:
import "./styles.css";
import { useEffect, useState } from "react";
import Alerts from "./alerts";
export default function App() {
const [alertList, setAlertList] = useState([]);
function addAlert(alert) {
const newAlertList = [...alertList, alert];
setAlertList(newAlertList);
}
function removeAlert(idx) {
const newAlertList = [...alertList];
newAlertList.splice(idx, 1);
setAlertList(newAlertList);
}
useEffect(() => {
addAlert({ level: "success", message: `A success alert.` });
addAlert({ level: "info", message: `An info alert.` });
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Alerts alertList={alertList} removeAlert={removeAlert} />
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
And alerts.js
:
function AlertItem(alert, idx, removeAlert) {
return (
<div className={"w-auto rounded-lg py-2 px-6 mt-2"} key={idx}>
<span>{alert.message}</span>
<button className="ml-2 cursor-pointer" onClick={() => removeAlert(idx)}>
x
</button>
</div>
);
}
export default function Alerts(props) {
const { alertList, removeAlert } = props;
return (
<div className="relative" id="alert-wrapper">
<div className="absolute right-10 text-right">
{alertList.length
? alertList.map((alert, idx) => AlertItem(alert, idx, removeAlert))
: null}
</div>
</div>
);
}
I used useEffect to create two alerts in the App for testing purpose (but I also tried in subcomponents in my real project, the effect is the same) but only one is displayed.
Using the debugger alertList
only has one item at most.
Can you spot why?
2
Answers
This is a common issue with React, since state isn’t actually updated immediately synchronously when you call
setState()
but the update is instead just scheduled to be performed on the next render. That means that when the you invokesetAlertList()
for the second timealertList
has not yet been updated, therefore the first update will effectively get lost.The React team have document this issue. See I’ve updated the state, but logging gives me the old value and State as a snapshot
To fix it use an arrow function which gives you the access to the previous state and the error is fixed. Here an example:
Problematic:
Fixed:
To illustrate what happens based on this example:
setData([...data, "first"])
evaluates tosetData(["first"])
sincedata
is initially set to[]
. React does NOT updatedata
yet, since that only happens on the next render, but not in the current render (= function invocation).setData([...data, "second"])
evaluates tosetData(["second"])
sincedata
is initially set to[]
and has not yet been updated for this render. Therefore React now has two updates scheduled fordata
only that the second overwrites the first therefore the new value fordata
is["second"]
According to the React-18-blog
To fix this issue what you can consider doing is to use the Functional Form of
setState
To fix this issue, you need to ensure that each update to
alertList
considers the current state at the time of the update. You can achieve this by using the functional form of setState, which takes the previous state as an argument and returns the new state.You should do this:
In addition -> credit to @moonstar :
add a
useEffect
to check array