updating list with useState more than once results in wrong output

Viewed 82

I am struggling with handling a list with useState in React. I have to add an item to the list twice in a row, and only the second item is added. The code:

App.js

import React, { useState } from "react";
import ItemForm from "./itemForm";

function App() {
  const [itemList, setItemList] = useState([]);

  function addItem(item) {
    setItemList([...itemList, item]);
  }

  return (
    <div>
      <ItemForm onAddItem={ addItem } />
      { itemList && 
          itemList.map((item, i) => <ul key={ i + itemList } >{ item }</ul>) }
    </div>
  );
}

export default App;

ItemForm.js

import React, { useState } from 'react';

function ItemForm({ onAddItem }) {
    const [inputValue, setInputValue] = useState('');

    function handleSubmit(e) {
        e.preventDefault();
        var item = e.target.item.value;
        // Here, I have to call onAddItem twice.
        onAddItem(item);
        onAddItem(item + " second time");
        
    }

    return (
        <div>
            <form onSubmit={ handleSubmit } >
                <input value={ inputValue }  
                onChange={(e) => setInputValue(e.target.value) }  name="item"></input>
                <button type="submit">Add</button>
            </form>
        </div>
    )
}

export default ItemForm;

This is the actual behavior of the app:

actual behavior

And this is the expected behavior:

expected behavior

why this happens and how can I fix it?

3 Answers

State updates are asynchronous

For your two calls,the itemsList array is exactly the same. :

Suppose you have an array : [1,2,3]. What your code essentially does is make it [1,2,3,4] for the first time, and [1,2,3,5] for the second time.

setItemList([...itemList, item]) //[1,2,3,4]
setItemList([...itemList, item]) //[1,2,3,5]

Your array in the second update is still using the original value.

One way to do this you can use this pattern which uses the previous state. This is a reliable way to use the previous state.

import React, { useState } from "react";
import ItemForm from "./itemForm";

function App() {
  const [itemList, setItemList] = useState([]);

  function addItem(item) {
    setItemList(itemList => ([...itemList, item]));
  }

  return (
    <div>
      <ItemForm onAddItem={ addItem } />
      { itemList && 
          itemList.map((item, i) => <ul key={ i + itemList } >{ item }</ul>) }
    </div>
  );
}

export default App;
import React, { useState } from 'react';

function ItemForm({ onAddItem }) {
    const [inputValue, setInputValue] = useState('');

    function handleSubmit(e) {
        e.preventDefault();
        var item = e.target.item.value;
        // Here, I have to call onAddItem twice.
        onAddItem(item);
        onAddItem(item + " second time");
        
    }

    return (
        <div>
            <form onSubmit={ handleSubmit } >
                <input value={ inputValue }  
                onChange={(e) => setInputValue(e.target.value) }  name="item"></input>
                <button type="submit">Add</button>
            </form>
        </div>
    )
}

export default ItemForm;

Function passed to ItemForm component as a prop, i.e. onAddItem, has a closure over the value of itemList that was in-effect when that function was created.

As a result, calling that function twice with a different value for item creates a new array with the same value of itemList that the addItem function closed over.

Example:

If the addItem function pass to the ItemForm component has a closure over the initial value of itemList, i.e. an empty array; then the following state setter function call:

setItemList([...itemList, item]);

spreads the empty array and then adds the new item as shown below.

setItemList([...[], item]);

Calling addItem function twice is same as:

setItemList([...[], item]);
setItemList([...[], item]);

Also note that the state is constant within a particular render of a component. Component can't see the updated state until it is re-rendered.

Solution

You can pass a function to setItemList to update the state based on the previous value of itemList:

function addItem(item) {
   setItemList(currentState => [...currentState, item]);
}

Alternate option is to pass an array to the onAddItem function:

onAddItem([item1, item2]);

and change the addItem function as:

function addItem(itemsArr) {
   setItemList([...itemList, ...itemsArr]);
}

React doesn't update the state immediately after you call setState, it takes all of setState calls, and runs them before re-render.

So in your case, the second onAddItem is overriding the first one.

To solve such issues, you need to change some logic in your code for example:

  function addItem(...newItems) {
    setItemList(previousItemList => ([...previousItemList, ...newItems]));
  }

Now you can pass multiple items to add item and call it only once.

 onAddItem(item, item + " second time");
Related