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?
1
According to the React-18-blog
So what you can do 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:
function addAlert(alert) {
setAlertList((prevAlertList) => [...prevAlertList, alert]);
}
Your update app.js would be this
import "./styles.css";
import { useEffect, useState } from "react";
import Alerts from "./alerts";
export default function App() {
const [alertList, setAlertList] = useState([]);
function addAlert(alert) {
setAlertList((prevAlertList) => [...prevAlertList, alert]);
}
function removeAlert(idx) {
setAlertList((prevAlertList) => prevAlertList.filter((_, i) => i !== idx));
}
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>
);
}
1
This is a common issue with React, since state isn’t actually updated immediately synchronously when you call setState()
but is instead just scheduled asynchronously. That means that when the you invoke setAlertList()
for the second time alertList
has not yet been updated, therefore the first update will 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:
const Demo = () => {
const [data, setData] = React.useState([]);
React.useEffect(() => {
setData([...data, "first"]);
setData([...data, "second"]);
}, []);
return (
<ul>
{data.map((x) => (
<li>{x}</li>
))}
</ul>
);
};
ReactDOM.render(<Demo/>, document.getElementById('root'));
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Fixed:
const Demo = () => {
const [data, setData] = React.useState([]);
React.useEffect(() => {
setData((prev) => [...prev, "first"]);
setData((prev) => [...prev, "second"]);
}, []);
return (
<ul>
{data.map((x) => (
<li>{x}</li>
))}
</ul>
);
};
ReactDOM.render(<Demo/>, document.getElementById('root'));
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<div id="root"></div>