skip to Main Content

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


  1. 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 invoke setAlertList() for the second time alertList 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:

    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>

    To illustrate what happens based on this example:

    1. setData([...data, "first"]) evaluates to setData(["first"]) since data is initially set to []. React does NOT update data yet, since that only happens on the next render, but not in the current render (= function invocation).
    2. setData([...data, "second"]) evaluates to setData(["second"]) since data is initially set to [] and has not yet been updated for this render. Therefore React now has two updates scheduled for data only that the second overwrites the first therefore the new value for data is ["second"]
    Login or Signup to reply.
  2. 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:

    function addAlert(alert) {
      setAlertList((prevAlertList) => [...prevAlertList, alert]);
    }
    

    In addition -> credit to @moonstar :
    add a useEffect to check array

    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(() => {
        if (alertList.length === 0) {
          addAlert({ level: "success", message: `A success alert.` });
          addAlert({ level: "info", message: `An info alert.` });
        }
      }, [alertList]);
    
      return (
        <div className="App">
          <h1>Hello CodeSandbox</h1>
          <Alerts alertList={alertList} removeAlert={removeAlert} />
          <h2>Start editing to see some magic happen!</h2>
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search