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:
And this is the expected behavior:
why this happens and how can I fix it?
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.
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");