React Dom not updating after updating a state array - javascript

so this function updates this state array:
let [Produits, setProduit] = useState(JSON.parse(Devis.produits))
let changeQte = (id, e) => {
let produittable = Produits;
produittable.forEach((p) => {
if (p.id == id) {
p.quantityAchete = parseInt(e.target.value);
}
});
setProduit(produittable);
};
the array did update without any problem but the changes aren't getting re-rendered
{console.log('rendering') ,Produits.map((p) => (
<div key={p.id} className="product_column flex_center">
<div className="productItem">{p.nom}</div>
<div className="productItem">{p.category}</div>
<div className="productItem">{p.prix_vente} DA</div>
<input
onChange={(e) => changeQte(p.id, e)}
type="number"
name="qte"
value={p.quantityAchete}
/>
as you can see i'm loggin to the console to check if that line is getting executed and it does ! but the values rendered doesn't update !

Don't mutate state, do this instead:
let changeQte = (id, e) => {
setProduit(existing => existing.map(c => c.id === id ? {...c,quantityAchete: parseInt(e.target.value)} : c))
};
These lines:
// this line just sets produittable to the same reference as Produits.
// so now produittable === Produits, it's essentially useless
let produittable = Produits;
produittable.forEach((p) => {
if (p.id == id) {
// you are mutating
p.quantityAchete = parseInt(e.target.value);
}
});
// because produittable === Produits, this doesn't do anything
setProduit(produittable);

In addition to what Adam said, besides not modifying the state directly, the reason you're not seeing any changes is because the component only gets rerendered when the state actually changed. And to know whether the state changed, react makes a shallow comparison between the two states. Since you modified the state directly, the reference remains the same and as such your component isn't rerendering.

To expand on Adam's answer, you can also clone the current state Produits and assign it to the local variable produittable and have the state update be recognized.
So instead of this:
let produittable = Produits;
You could simply clone it like so, with the spread operator:
let produittable = [...Produits];

Related

React state setter function not changing the state

I am working on a history component in my React project. I have the history state and a setter function setHistory() . I have two functions for handling clicks: handleClick() and removeHistoryItem(), which are called with different buttons. In both of them, I am changing the state with the setHistory() function, but it doesn't work in the handleClick() one.
const removeHistoryItem = () => {
const newHistory = history.filter(item => item.id != id)
setHistory(newHistory)
}
const handleClick = () => {
let newHistory = history
let [selectedGame] = history.filter(item => item.id == id)
const index = history.indexOf(selectedGame)
selectedGame.isFavorite = !selectedGame.isFavorite
newHistory.splice(index, 1, selectedGame)
localStorage.setItem('history', JSON.stringify(newHistory))
setHistory(newHistory)
}
The functions are next to each other so there shouldn't be a problem with the scope. Both functions are executing the other operations well (tested with console.log(), the localStorage is changed just fine, the splice and filter functions are working also). Even the value I am passing to the setHistory() function is correct, just the state is not changed. It's like the setHistory() function is not even called. I am not getting any errors, what could the problem be?
Error is due to Reference equality, both newHistory and history in your handleClick function are referencing same array in "memory", which you mutate with splice, but it is still the same object, react will not detect the change and will not fire the rerender, just because oldValue (history) === newValue (newHistory).
So you need to "recreate" the array in this case. For removeHistoryItem all is ok due to .filter returns the new array already, so new reference, new object.
setHistory([...newHistory]);

react setState() not triggering re-render

I have component which receives data from other file and setting into state:
const [sortedPlans, setSortedPlans] = useState(
data.Plans.sort((a, b) => b.IndustryGrade - a.IndustryGrade)
);//data is from external file
After setting the state, sorting the data and rendering the initial screen, I have function that sorts the sortedPlans whenever it is called:
const sort = (event) => {
console.log(event);
const newArr = sortedPlans.sort((a, b) => {
return b[event] - a[event];
});
console.log(newArr);
return setSortedPlans(newArr);
};
The problem is that this is never triggering a re-render of the component. I want when I set the new state to be able to see it inside the jsx. Why when I console.log(newArr) the results are correctly sorted but this setting of the state not triggering re-render? Here is the sandbox:
https://codesandbox.io/s/charming-shape-r9ps3p?file=/src/App.js
Here you go: Codesandbox demo.
You should first make a shallow copy of the array you want to modify. Then set that array as the new state. Then it re-renders the component and you are able to filter like you want.
const sort = (event) => {
console.log(event);
//shallow copying the state.
const newArr = [...sortedPlans];
//modifying the copy
newArr.sort((a, b) => {
return b[event] - a[event];
});
console.log(newArr); //results here are correctly sorted as per event
//setting the state to the copy triggers the re-render.
return setSortedPlans(newArr);
};

ReactJS: Component doesn't rerender on state change

I am trying to update state on click event using react hooks. State changes, but component doesn't rerender. Here is my code snippet:
function ThirdPage() {
const [selectedIngredients, setSelectedIngredients] = useState([])
const DeleteIngredient = (ingredient) => {
let selectedIngredientsContainer = selectedIngredients;
selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1);
setSelectedIngredients(selectedIngredientsContainer);
console.log(selectedIngredients);
}
const selectedIngredientsDiv = selectedIngredients.map(ingredient =>
(
<div className={styles.selectedIngredientsDiv}>{ingredient}
<div className={styles.DeleteIngredient}
onClick={() => {
DeleteIngredient(ingredient)}}>x</div></div>
))
return (
...
What am I doing wrong? Thanks in advance!
Issue with you splice as its not being saved to selectedIngredientsContainer. I would do following:
selectedIngredientsContainer = selectedIngredientsContainer.filter(value => value !== ingredient);
or
selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1 );
setSelectedIngredients([...selectedIngredientsContainer]);
Hope it helps.
normally I would leave an explanation on what's going on but tldr is that you should check first to make sure that you're array isn't empty, then you you can filter out the currentIngredients. Also you don't need curly brackets to call that function in the jsx but that can be personal flavor for personal code. I apologize if this doesn't help but I have to head out to work. Good luck!
function ThirdPage() {
const [selectedIngredients, setSelectedIngredients] = useState([]);
const DeleteIngredient = ingredient => {
// let selectedIngredientsContainer = selectedIngredients;
// selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1);
// setSelectedIngredients(selectedIngredientsContainer);
// console.log(selectedIngredients);
if (selectedIngredients.length > 0) {
// this assumes that there is an id property but you could compare whatever you want in the Array.filter() methods
const filteredIngredients = setSelectedIngredients.filter(selectedIngredient => selectedIngredient.id !== ingredient.id);
setSelectedIngredients(filteredIngredients);
}
// nothing in ingredients - default logic so whatever you want
// log something for your sanity so you know the array is empty
return;
};
const selectedIngredientsDiv = selectedIngredients.map(ingredient => (
<div className={styles.selectedIngredientsDiv}>
{ingredient}
<div className={styles.DeleteIngredient} onClick={() => DeleteIngredient(ingredient)}>
x
</div>
</div>
));
}
The answer is very Simple, your state array selectedIngredients is initialized with an empty array, so when you call map on the empty array, it will not even run once and thus DeleteIngredient is never called and your state does not change, thus no re-render happens

useState Object's value is always the initial value

I have code where if a function is invoked it will call toggleCheckedUser and pass along information about which object property to toggle. Then saves the modified object back to state (selectedSendTo).
However, when I run this, the toggle it works, but when I try to edit a second property, before changing it I try console.log(selectedSendTo) I always get the initial value whether it be an empty object {} or false instead of the previously updated object.
When I use useEffect to spy on selectedSendTo I can see that the setSelectedSendTo() function correctly updated the object. So for some reason when I revisit the object it's empty.
const [selectedSendTo, setSelectedSendTo] = useState(false);
const toggleCheckedUser = (companyID, contactID) => {
console.log(companyID, contactID);
console.log(selectedSendTo); // THIS VALUE IS always the same as INITIAL value
console.log(selectedSendTo[companyID]);
if(selectedSendTo[companyID] &&
selectedSendTo[companyID][contactID] === true){
//remove it
delete(selectedSendTo[companyID][contactID]);
}else{
setSelectedSendTo({
...selectedSendTo,
[companyID]:{
...selectedSendTo[companyID],
[contactID]: true,
}
})
}
}
Here is the DOM:
<CustomCheckbox
className="person__checkbox" name={`checkbox-${contactID}`}
alreadySelected={
selectedSendTo[company.companyID] &&
selectedSendTo[company.companyID][contactID]
}
onChange={() => toggleCheckedUser(company.companyID, contactID)}
/>
UPDATE, A POSSIBLE SOLUTION
I found that the following works:
To be able to access the current value from useState I used useRef
const selectedSendToRef = useRef();
useEffect( () => {
selectedSendToRef.current = selectedSendTo;
}, [selectedSendTo])
Then inside of my function, I can use selectedSendToRef.current to access the most recent value of `selectedSendTo.
When updating state, I can access the most recent version from state using
setSelectedSendTo( prevValue => ....)
const toggleCheckedUser = (companyID, contactID) => {
console.log(companyID, contactID, selectedSendToRef.current);
console.log('selectedSendTo[companyID]: ', selectedSendTo[companyID]);
let newValue;
if(selectedSendToRef.current[companyID] &&
selectedSendToRef.current[companyID][contactID] === true){
newValue = false;
}else{
newValue = true;
}
setSelectedSendTo(prevValue => (
{
...prevValue,
[companyID]:{
...prevValue[companyID],
[contactID]: newValue,
}
}
));
}
UPDATE 2: The Real Solution
Okay so it seems like the problem was that even after a render, the child component was not receiving the updated state because of how I had used nested functions to create the elements.
Here is how I had things
<Main Component>
<div>
{Object_1}
<div>
</Main Componenent
and object_1 was defined something like this:
const Object_1 =
<React.Fragment>
<h1>Random Header</h1>
{StateObject_Containg_Elements}
</React.Fragment>
Now to create the state object that conatined the elements I wanted to display I was using a funciton called by a useEffect hook. Basically when the server sent back data that I needed, I would tell the useEffect hook to run a function called createElements
const createElements = (data) => {
const elements = Object.keys(data).map( item => return(
<ul>
{subFunction1(item)}
</ul>
subFunction1(item){
item.contacts.map( name => {
reutrn <CustomCheckbox name={name} checked={selectedSendTo[name]}
})
}
saveElementsToState(elements);
}
As you can see we basically have a function that runs 1 time (on server response) that triggers a function that creates the array of elements that we want to display which has its own nested subfunction that includes the child component that we are asking to watch a different state object to know whether it should be checked or not.
So What I did was simplify things, I turned {Object_1} into it's own functional component, lets call it <Object1 />. Inside the component instead of calling a function I just put the function code in there to loop through and return the elements (no longer saving elements to state) and lastly I no longer needed the useEffect since just updating the state object with the data once it gets it from the server would cause my subcomponent to re-render and create the elements. Inside the sub-component I simply return null if the data in state is null.
That fixed all my problems.
so now it looks something like this:
const Object1 = () => {
if(!data)return null;
return(
Object.keys(data).map( item => return(
<ul>
{subFunction1(item)}
</ul>
subFunction1(item){
item.contacts.map( name => {
reutrn <CustomCheckbox name={name} checked={selectedSendTo[name]}
})
}
)
}
return(
<div>
<Object1 /> //This is what contains/creates the elements now
</div>
)

Looking for assistance in getting proper results from .filter()

I am trying to filter an array with a string that is input by user. The results are not updating properly with the first key input, then if the box is cleared or characters removed/changed, results that may now pass the filter are not being displayed.
The goal is to have all results displayed on initial page render, then properly updated with each keystroke.
Apologies; I'm just learning to code. Thanks for all assistance.
searchCompUsers = () => {
const newState = {}
const filteredEmps = this.props.employees.filter(
user => user.name.includes(this.state.searchName)
)
console.log(filteredEmps)
`` newState.filterEmps = filteredEmps
this.setState(newState)
}
empSearch = evt => {
const stateToChange = {};
stateToChange[evt.target.id] = evt.target.value;
this.setState(stateToChange);
this.searchCompUsers()
};
These lines are being run in sequence:
this.setState(stateToChange);
this.searchCompUsers();
...
const filteredEmps = this.props.employees.filter(
user => user.name.includes(this.state.searchName)
)
...
this.setState(newState);
I am assuming in your example, evt.target.id is searchName.
Two things you're doing here which you shouldn't do:
Running two setStates in sequence. This isn't necessarily a problem, but there's generally no reason for it and it could mean your code is structured poorly.
Referencing the state immediately after setState. setState is run asynchronously, so you can't guarantee the state will be updated by the time you reach your filter.
The weird results you're getting are probably stemming from (2).
Something like this would work better, assuming the rest of your code is fine:
empSearch = evt => {
const key = evt.target.id;
const value = evt.target.value;
if (key === "searchName") {
const filteredEmps = this.props.employees.filter(
user => user.name.includes(value);
);
this.setState({
filterEmps: filteredEmps
});
}
};
This way, you're only calling setState once per event, and you're not relying on the results of an earlier setState.
If you need to keep searchName in the state for some reason (such as using a controlled component), then you can simply add it to the same setState.
this.setState({
filterEmps: filteredEmps,
searchName: value
});
The only places you can assume the state is up-to-date is in the render() function, and in certain React lifecycle functions. You can also provide a callback to setState if necessary, though this should be relatively rare: this.setState({ ...someState }, () => { ...someCodeToRun() });

Categories