Updating an array of objects with setTimeout with react and state - javascript

I have an array of objects, and each object has a false property value by default:
const [items, setItems] = useState([ { key: 1, has_x: false }, { key: 2, has_x: false }, { key: 3, has_x: false } ]);
Which is passed down to child components:
<Item key={item.key} hasX={item.has_x} />
In the parent component, I have a "Check All" button:
<button onClick={handleCheckAll}>Check All</button>
Which would loop through every item and modify item.has_x to true.
In the child components, there's also a "Check" button, but instead of checking all items, it just checks and sets that one specific item has_x to true.
I think I would know how to do each one. For the "Check All" function, I'd create a shadow copy, set the value, and then once the loop is done, set the state. For the child button, I really just need a useState there for it.
However, I am stuck on the "Check All" button because I'd like to have ui updates as has_x for each item gets updated and a setTimeout, as the actual functionality of check all will be expensive and needs about 200-300ms wait time for each check. Imagine a button's text changes to a checkmark as the function loops through each item.
Here's my attempt but the UI doesn't get updated and I only am setting the state once it's done:
const checkAllItems = () => {
let temp = [...items];
temp.map((item, index) => {
setTimeout(() => {
let tempEl = {...tempState[index]};
if (tempEl) item.has_x = true;
}, index * 200)
})
setItems(temp)
}
My only idea of how to do this is to use refs and loop through the refs to run a function in each child component that will do this, but I feel like there's a correct way to go about this. How is it possible?

I can't imagine needing a timeout for updating state. I am certain if you tweak your check-all items code a bit you won't need the timeout. You can use a functional state update to enqueue the state updates and correctly update from the previous state instead of the state value closed over in callback scope.
const checkAllItems = () => {
setItems(items => items.map(item => ({
...item,
has_x: true,
})));
};
This shallow copies the previous items state into a new array, and then you also shallow copy each item element and update the has_x property.
Update
If you are making backend API requests per checked update in items then I suggest doing this in a useEffect lifecycle hook. Loop over each item and enqueue the network request in a setTimeout. It's basically splitting out the timeout logic (you had previously) from the state update.
useEffect(() => {
items.forEach((item, index) => {
setTimeout(() => {
// make API request with `item`
}, index * 200);
});
}, [items]);
/update
I also don't recommend the child components to also have any "checked" state, you want a single source of truth as to the checked status of your items array. Pass a callback to the children for them to update their checked status in the parent component's state.
It could look something like this:
const updateStatus = (key, value) => {
setItems(items => items.map(item => item.key === key
? {
...item,
has_x: value,
}
: item
));
};
...
<Item key={item.key} hasX={item.has_x} updateX={updateStatus} />

Related

Why does calling one `setState` function update an entirely separate state value?

I have an issue where one setState function is updating two separate state values.
I am trying to make a small React sortable array. The program should behave like so:
fetches data on mount
stores that data in both unsortedData & displayedData state hooks
when a user clicks "toggle sorting" button the array in displayedData is sorted (using .sort())
on a second click of "toggle sorting" it should set displayedData to be the same value as unsortedData - restoring the original order
However, on first click of toggle sorting, it sorts both unsortedData & displayedData, which means I lose the original data order. I understand I could store their order, but I'm wanting to know why these state values appear to be coupled.
Stackblitz working code here.
GIF showing issue here
I cannot find where this is happening. I can't see any object/array referencing going on (I'm spreading for new objects/arrays).
Code here:
const Test = () => {
const [unsortedData, setUnsortedData] = useState([])
const [displayedData, setDisplayedData] = useState([])
const [isSorted, setisSorted] = useState(false)
const handleSorting = () => setisSorted(!isSorted)
useEffect(() => {
if (isSorted === true) setDisplayedData([...unsortedData.sort()]) // sort data
if (isSorted === false) setDisplayedData([...unsortedData]) // restore original data order
}, [isSorted, unsortedData])
useEffect(() => {
const mockData = [3, 9, 6]
setUnsortedData([...mockData]) // store original data order in "unsortedData"
setDisplayedData([...mockData])
}, [])
return (
<div>
{displayedData.map(item => item)}
<br />
<button onClick={() => handleSorting()}>Toggle sorting</button>
</div>
)
}
The .sort function mutates the array, so when you do this:
setDisplayedData([...unsortedData.sort()])
You are mutating unsortedData, then making a copy of it afterwards. Since you mutated the original array, that change can display on the screen when the component rerenders.
So a minimum fix would be to copy first, and sort afterwards:
setDisplayedData([...unsortedData].sort())
Also, why do I need the useEffect? Why can't I just move the contents of the useEffect (that has the if statements in) into the handleSorting function?
Moving it into handleSorting should be possible, but actually i'd like to propose another option: have only two states, the unsorted data, and the boolean of whether to sort it. The displayed data is then a calculated value based on those two.
const [unsortedData, setUnsortedData] = useState([3, 9, 6]) // initialize with mock data
const [isSorted, setisSorted] = useState(false)
const displayedData = useMemo(() => {
if (isSorted) {
return [...unsortedData].sort();
} else {
return unsortedData
}
}, [unsortedData, isSorted]);
const handleSorting = () => setisSorted(!isSorted)
// No use useEffect to sort the data
// Also no useEffect for the mock data, since i did that when initializing the state
The benefits of this approach are that
You don't need to do a double render. The original version sets isSorted, renders, then sets displayed data, and renders again
It's impossible to have mismatched states. For example, in between that first and second render, isSorted is true, and yet the displayed data isn't actually sorted yet. But even without the double render case, having multiple states requires you to be vigilant that every time you update unsortedData or isSorted, you also remember to update displayedData. That just happens automatically if it's a calculated value instead of an independent state.
If you want to display sorted array to user only when the user presses the button, you could use this code statement (orignaly coppied from vue3 todo list)
const filters = {
none: (data) => [...data],
sorted: (data) => [...data].sort((a, b) => a - b),
}
const Test = () => {
const [unsortedData, setUnsortedData] = useState([])
const [currentFilter, setCurrentFilter] = useState('none')
useEffect(() => {
const mockData = [3, 9, 6]
setUnsortedData([...mockData])
}, [])
return (
<div>
<ul>
{/* All magic is going here! */}
{filters[currentFilter](unsortedData).map(item => <li key={item}>{item}</li>)}
</ul>
<br />
<button
onClick={() =>
setCurrentFilter(currentFilter === 'none' ? 'sorted' : 'none')}>
Toggle sorting
</button>
</div>
)
}

UseState called by UseEffect doesn't update the variable using the Set method

Consider the code :
import React, { useState, useEffect } from 'react';
........ More stuff
const ProductContext = React.createContext();
const ProductConsumer = ProductContext.Consumer;
const ProductProvider = ({ children }) => {
const [state, setState] = useState({
sideBarOpen: false,
cartOpen: true,
cartItems: 10,
links: linkData,
socialIcons: socialData,
cart: [],
cartSubTotal: 0,
cartTax: 0,
cartTotal: 0,
.......
loading: true,
cartCounter: 0,
});
const getTotals = () => {
// .. Do some calculations ....
return {
cartItems,
subTotal,
tax,
total,
};
};
const addTotals = () => {
const totals = getTotals();
setState({
...state,
cartItems: totals.cartItems,
cartSubTotal: totals.subTotal,
cartTax: totals.tax,
cartTotal: totals.total,
});
};
/**
* Use Effect only when cart has been changed
*/
useEffect(() => {
if (state.cartCounter > 0) {
addTotals();
syncStorage();
openCart();
}
}, [state.cartCounter]);
..... More code
return (
<ProductContext.Provider
value={{
...state,
............... More stuff
}}
>
{children}
</ProductContext.Provider>
);
};
export { ProductProvider, ProductConsumer };
This is a Context of a Shopping cart ,whenever the user add a new item to the cart
this piece of code runs :
useEffect(() => {
if (state.cartCounter > 0) {
addTotals();
syncStorage();
openCart();
}
}, [state.cartCounter]);
And updates the state , however the setState function doesn't update state
when running :
setState({
...state,
cartItems: totals.cartItems,
cartSubTotal: totals.subTotal,
cartTax: totals.tax,
cartTotal: totals.total,
});
Inside addTotals , even though this function is being called automatically when UseEffect detects that state.cartCounter has been changed.
Why aren't the changes being reflected in the state variable ?
Without a stripped down working example, I can only guess at the problems...
Potential Problem 1
You're calling a callback function in useEffect which should be added to it's [dependencies] for memoization.
const dep2 = React.useCallback(() => {}, []);
useEffect(() => {
if(dep1 > 0) {
dep2();
}
}, [dep1, dep2]);
Since dep2 is a callback function, if it's not wrapped in a React.useCallback, then it could potentially cause an infinite re-render if it's changed.
Potential Problem 2
You're mutating the state object or one of its properties. Since I'm not seeing the full code, this is only an assumption. But Array methods like: splice, push, unshift, shift, pop, sort to name a few cause mutations to the original Array. In addition, objects can be mutated by using delete prop or obj.name = "example" or obj["total"] = 2. Again, without the full code, it's just a guess.
Potential Problem 3
You're attempting to spread stale state when it's executed. When using multiple setState calls to update an object, there's no guarantee that the state is going to be up-to-date when it's executed. Best practice is to pass setState a function which accepts the current state as an argument and returns an updated state object:
setState(prevState => ({
...prevState,
prop1: prevState.prop1 + 1
}));
This ensures the state is always up-to-date when it's being batch executed. For example, if the first setState updates cartTotal: 11, then prevState.cartTotal is guaranteed to be 11 when the next setState is executed.
Potential Problem 4
If state.cartCounter is ever updated within this component, then this will cause an infinite re-render loop because the useEffect listens and fires every time it changes. This may or may not be a problem within your project, but it's something to be aware of. A workaround is to trigger a boolean to prevent addTotals from executing more than once. Since the prop name "cartCounter" is a number and is rather ambiguous to its overall functionality, then it may not be the best way to update the cart totals synchronously.
React.useEffect(() => {
if (state.cartCounter > 0 && state.updateCart) {
addTotals();
...etc
}
}, [state.updateCart, state.cartCounter, addTotals]);
Working demo (click the Add to Cart button to update cart state):
If neither of the problems mentioned above solves your problem, then I'd recommend creating a mwe. Otherwise, it's a guessing game.

How to sort React components based on specific value?

I have React component. This components take 'units' - (array of objects) prop. Based on that I render component for each of item. I want to sort my components based on 'price' value, which is one of state items property. But when i trigger the sorting - state changes correctly but my components order not changing.
const SearchBoxes = ({units}) => {
const [unitsState, setUnitsState] = useState([])
useEffect(() => {
setUnitsState(units)
}, [units])
const sortByPrice = () => {
const sortedUnits = sort(unitsState).desc(u => u.price); // sorting is correct
setUnitsState(sortedUnits) // state is changing correctly
}
return (
<div>
{unitsState.map((u,i) => {
return <UnitBox key={u.price} unit={u} />
})}
</div>
)
}
Can somebody help me, please ?
Why my components order do not changing when the state is changing after sort triggering ?
You aren't calling sortByPrice anywhere--all you've done is to define the function. I haven't tried it, but what if you changed useEffect to:
useEffect(() => {
setUnitsState(sort(unitsState).desc(u => u.price));
}, [units])
Then you don't need the sort method at all.

Unable to correctly update parent object state

I have multiple child components and need to communicate between these siblings. What im currently trying:
Parent has object state and pass setMessageItem as a prop to all children:
const [messageItem, setMessageItem] = useState({})
Children:
useEffect(() => {
if(condition) {
props.setMessageItem(prevState => ({...prevState, messageData}))
..}
}
}[])
However only data from one children reaches this object state. Is there any way I could make it work when two or more children are trying to change the parent's state at the same time?
So the end result would be that messageData would contain data from multiple children at the same time.
That useEffect() will only be run once, when the component is instantiated. If you want to use it like that you would do:
useEffect(() => {
props.setMessageItem(prevState => ({ ...prevState, messageData }));
}, [condition]);
Then it would trigger on each change of condition.

React not mapping certain objects in an array

I have to dynamically render an input form, based on the selection of a radio button. I have an array that is incremented every time the user select an radio button.
The problem is: I append an object to the array, and try to map that array on render() function. The map apparently is ignoring the object that I insert.
The select radio button code:
<MDBInput
onClick={() => {
let dependentFullName = dependent.dependentFullName;
let dependentAnswerList = this.state[dependentFullName];
let newQuestionData = {
question: thing.pergunta,
answer: true,
answerRaised: true,
info: ''
};
dependentAnswerList[thing.pergunta] = newQuestionData;
this.setState({
[dependentFullName]: dependentAnswerList
})
}}
checked={this.state[dependent.dependentFullName][thing.pergunta] ? this.state[dependent.dependentFullName][thing.pergunta]["answer"] ? true : false : false}
label='Sim'
type='radio'
id={"holder." + thing.pergunta}
/>
The map rendering code prototype:
{this.state.holder.map((question) => (<React.Fragment>
<h5>{question}</h5> <h5>{question.question}</h5></React.Fragment>))}
Try setting a state with a new reference of your mutated array:
this.setState({
[dependentFullName]: [...dependentAnswerList]
});
On state change, react makes shallow comparison with the previous one, in your case it has the same reference, therefore no render triggered.
What does setState do?.
setState() schedules an update to a component’s state object. When state changes, the component responds by re-rendering.
From setState API:
Both state and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with the state.
A possible full-fix may look like:
const onClick = () => {
const dependentFullName = dependent.dependentFullName;
const dependentAnswerList = this.state[dependentFullName];
const newQuestionData = {
question: thing.pergunta,
answer: true,
answerRaised: true,
info: ''
};
this.setState({
[dependentFullName]: {
...dependentAnswerList,
[thing.pergunta]: newQuestionData
}
});
};
<MDBInput onClick={onClick} {...rest}/>;

Categories

Resources