Why component in react dont re-rendering - reactjs

Hello why component in react was not rendering? State was updated and i see it in developer tools but content was not changing
FULL CODE: https://pastebin.com/bxNUAieV
import React, {useState} from 'react'
const List = (props:any) => {
const [actToDo, changeActTodo] = useState(props.actToDo)
const [ToDoLists, changeActToDoLists] = useState(props.ToDoLists)
return (
<>
{ToDoLists[actToDo].list.map((e:any, index:any) => (
<div className={'Todo__element'}>
<li key={index}>{e}</li><i className="fas fa-check"></i><i className="fas fa-recycle" onClick={() => props.removeElement(index)}></i>
</div>))}
</>
)
}
export default List
ToDoLists[number].list save the list
actToDo save number
const removeElement = (index:any) => {
console.log(index);
let FullList = ToDoLists
//#ts-ignore
FullList[actToDo].list.splice(index,1)
changeToDoLists(FullList)
console.log(ToDoLists);
}

you are mutating the array, that will not work, also the console.log will display the wrong value as setState is async.
https://dev.to/il3ven/common-error-accidentally-mutating-state-in-react-4ndg
const removeElement = (index:any) => {
console.log(index);
let FullList = {
...ToDoLists,
[actToDo]: {
...ToDoLists[actToDo],
list: ToDoLists[actToDo].list.splice(index,1)
}
}
console.log(FullList);
changeToDoLists(FullList);
}
by the way, saving props to state is a bad behavior, as if the prop change nothing will happen.

What I can see in your code is that you are directly modifying a state variable, which we should never do in React. To modify a state variable first make its copy in a new variable, and after making changes , pass that new variable in setState function :
So instead of :
let FullList = ToDoLists
do like this :
let FullList = _.cloneDeep(objects); //(using Lodash deep clone method here)
changeToDoLists(FullList);
Also , if you want to console your new list, then you should use useContext hook, since useState acts asynchronously and will not update value immediately, so console on very next line will not give updated value :
useEffect(() => {
console.log('Here is my new list after updation', ToDoLists);
}, [ToDoLists]);

This is happening because, you are directly trying to update the same state array.
React compares the previous state with the updated state to decide if the component needs to be re-rendered. Modifying the state directly will disturb this process.
While updating:
Since you are updating the data inplace using splice at the previous state's address i.e. mutating the same array and the updating state, and the state TodoLists is a complex object (nested array), so, React cant track the update and thus not detecting the state change.
While creating new item
This is not happening at the time of new item creation as you are not directly appending/ mutating the old state, therefore, the state got updating and react detects state change.
For removing the items, you can do is
const removeElement = (index) => {
console.log(index);
let FullList = [...ToDoLists]; //<-- create copy of the list
//#ts-ignore
FullList[actToDo].list = [...FullList[actToDo]].list.filter( //<-- filter the list by removing toBeRemoved Object
(_, i) => i !== index
);
console.log(FullList[actToDo]);
changeToDoLists(FullList); //update the state
};
Note: Whi;e updating a complex object, always keep in mind to use ... operator, which creates a shallow copy of your object for updating. It works both for array and object.

Related

Why in one function state is updated immidietly and in other isn't

so I know that setState() is asonchrynus but I don't understand when It is. I always run into this problem when mapping lists in react. Specifically when I delete list element.
This function updates state instantly and rerenders array.map() function:
const handleHistoryDel = (index) => {
setHistory(history.filter((_, i) => i !== index));
};
And this one updates state but doesn't rerender:
const handleHistoryDel = (index) => {
let temp=history;
temp.splice(index,1)
setHistory(temp);
};
What is the difference? Should second function use some kind of callback? If so how would you implement one?
This happens because temp has the same memory address reference, when you run array.filter you get a new array, allocated in another space.
when you write let temp = history; you get the same reference, React will compare the previous state with the current one, if they have the same reference, it will not re-render.
try yourself:
const handleHistoryDel = (index) => {
let temp = [...history];
temp.splice(index,1)
setHistory(temp);
};
Maybe pass a function inside the setState like this,
setHistory(prev => // Do some changes here and return it // )
setHistory(prev => [...prev])
Also refer to this answer that explains about immutablity
How does React useState hook work with mutable objects
You should create a new array without reference to the state's array.
And then mutate the newly created array.
const handleHistoryDel = (index) => {
let temp=[...history];
temp.splice(index,1)
setHistory(temp);
};

How update state in parent component from child, child component?

(sory for my english)
Hi, this code working half to half. I can delete product but i must go to another page , and back , to rerender. i dont know why ?
Code:
Parent Component:
function ShoppingCard() {
const shoppingCard = useContext<any>(ShoppingCardContext);
const [shoppingCard2, setShoppingCard2] = useState<any>(shoppingCard);
useEffect(() => {
setShoppingCard2(shoppingCard);
}, [shoppingCard, shoppingCard2]);
const removeProduct = (shopCart: any) => {
let index = shoppingCard.findIndex(
(oneProduct: any) => oneProduct.idProduct === shopCart.idProduct
);
shoppingCard.splice(index, 1);
setShoppingCard2(shoppingCard);
};
// passes this function to the next component through the next component.
Parent component 1
child component 2
child Component 3
And i use this fuction in a button to remove element. In console i see that shoppingCard and shoppingCard2 is update but only in child 3. And to see the efect (rerender) i must go to another page and back. In app i use React-Router.
Ps. This is my first post
Issue
You are mutating the state when deleting from the shoppingCard2 array, and then never creating a new array reference for React so reconciliation works. In other words, React doesn't "see" that the shoppingCard2 array is a new array so it doesn't trigger a rerender. You see the mutation when navigating away and back to the page.
const removeProduct = (shopCart: any) => {
let index = shoppingCard.findIndex(
(oneProduct: any) => oneProduct.idProduct === shopCart.idProduct
);
shoppingCard.splice(index, 1); // <-- mutation!!
setShoppingCard2(shoppingCard); // <-- same array reference
};
Solution
Shallow copy the shoppingCard2 array and remove the element. Use Array.prototype.filter to accomplish this.
Example:
const removeProduct = (shopCart: any) => {
setShoppingCard2(shoppingCard => shoppingCard.filter(
(oneProduct: any) => oneProduct.idProduct !== shopCart.idProduct
));
};
Suggestion
An improvement would be to expose a state updater function from the ShoppingCardContext so it can maintain the state invariant instead of offloading this to consumers. There's really no reason to "duplicate" the shopping card state locally if it can be helped.
The best will be to use a global state. You can read more about
React Context
Redux
another state manager

ReactJS useState hook: Can I update array of objects using the state variable itself?

I have:
const [list, setList] = useState([]);
Somewhere in the component, I want to do:
list[index][someKey] = some_updated_value;
setList(list);
Is this valid?
Or, is it better to do:
const newList = list;
newList[index][someKey] = some_updated_value;
setList(newList);
If this is better, Why?
Both implementations would be considered state mutations and should be avoided as this is anti-pattern in React. React uses shallow reference equality checks as part of its Reconciliation process, i.e. if a chunk of state is the same object reference as the previous render cycle it's assumed the value is the same and rerendering is skipped.
Version 1
This is just directly mutating the state object.
list[index][someKey] = some_updated_value; // <-- mutation!
setList(list);
Version 2
This also directly mutates the state object since newList is a reference to list.
const newList = list; // <-- newList reference to state
newList[index][someKey] = some_updated_value; // <-- mutation!
setList(newList);
The idea with React state updates is to apply the Immutable Update Pattern. This is where you create shallow copies of any state and nested state that is being updated. This is also usually combined with functional state updates where you pass a callback function to the updater function that is passed the previous state to update from. This helps avoid stale state enclosures.
Example:
setList(list => list.map((item, i) => // Array.protptype.map creates new array
i === index
? { // new object reference for updated item
...item, // shallow copy previous item state
[someKey]: newValue // overwrite property being updated
}
: item
));
Good docs:
Immutable Update Pattern - this is a Redux doc but provides a fantastic explanation and example.
Functional State Updates
I could be wrong but I think either way is mutating the state directly. I think it would be better to do like:
setList(list.map((item,i)=>{
if(index === i){
return {...item, [someKey]: some_update_value};
}
return item;
}))

Append an object to an array, why does "push" not work?

I just want to understand why line 33 does not work (marked out), but line 32 does work. Why am I unable to simply push an object to the array? Shouldn't these two lines (line 32 and 33) do the exact same thing?
Reference: https://codesandbox.io/s/lingering-wood-33vtb?file=/src/App.js:0-927
import React, { useState } from "react";
function ThingsToDo({ thing }) {
return <div>{thing.text}</div>;
}
function ToDoForm({ add }) {
const [value, setValue] = useState("");
const updateKey = (e) => {
e.preventDefault();
if (!value) return;
add(value);
setValue("");
};
return (
<form onSubmit={updateKey}>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</form>
);
}
function App() {
const [todos, setTodos] = useState([{ text: "Hi" }, { text: "Bye" }]);
const Addtolist = (text) => {
const newToDo = [...todos, { text }];
// const newToDo = todos.push({text})
setTodos(newToDo);
};
return (
<div>
{todos.map((todo, index) => (
<div>
<ThingsToDo thing={todo} />
</div>
))}
<ToDoForm add={Addtolist} />
</div>
);
}
export default App;
You can't mutate a state directly in react. In your 2 cases:
const newToDo = [...todos, { text }];
This one creates a copy of the todos array and adds your extra item. newTodo is a copy and then when you setTodos(newTodo) you set the state to the new copied array.
const newToDo = todos.push({text})
This one tries to push {text} into todos which is a state. You can't mutate a state directly you have to do this through setTodos().
If you are really set on using push then you can copy the state of todos first then push into the copied array but I think this is extra unecessary work.
You have to do as following,
// const newToDo = [...todos, { text }];
const newToDo = JSON.parse(JSON.stringify(todos));
newToDo.push({ text });
setTodos(newToDo);
Reasons:
You are attempting to mutate the state variable todos (will not work), you have to use setState
If you really want to mutate and set new state, you have to clone using JSON.parse, JSON.strigify, you can't create a real clone otherwise
As official React document https://reactjs.org/docs/react-component.html says,
NEVER mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
You are trying to push an object inside React state array which violates the immutability.
To update the react state, you will definitely have to use setState (in your case setTodos since you are building functional component).
And another thing is when you push the array and assign it to a const variable, it will assign the length of the array instead of the array which is what you wanted.
As w3school page says in https://www.w3schools.com/jsref/jsref_push.asp
The push() method adds new items to the end of an array, and returns
the new length.
My alternative to solving this problem is to use .concat method of Array type because it will not mutate the original array but it will just return a new array with concatted objects instead.
Example.
setTodos(todos.concat(text));
Ref:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat
1st case
That is not going to work. To trigger the state change you must use the setState to update the state. With the todo.push({ text }) it will update the state but never going to trigger the state changes.
2nd case
push returns the item you added to the array. So in setState you are try to override the current state array with { text : "somText" }. So now in your render method you can't map through the state array & you are getting an Error.
3rd case
You need to understand that arrays are reference type. So even you did something like below, it is directly update the todo array since this is not a real copy.
let copy = todo;
copy.push({text});
setTodo(copy)
There are two ways that you can copy.
Shallow Copy
Deep Copy
In Shallow Copy you only copies the values and the prototype will still refer to the original object.
In deep copy it copies everything. Even the references are copied.
In your case shallow copy is enough for perform the state update. You can do the shallow copy by [...todos, {text}]
In JavaScript if you need to do a deep copy, an easy way to do is,
const deepCpy = JSON.parse(JSON.stringiy(todos))
Also you can use deepCopy method in lodash

Hooks setter not setting state with object variable

I'm trying to build a simple tree menu with react. I found this video showing exactly what I want to achieve (his Codepen). I was able to implement his approach, but then, out of curiosity, tried to replicate the same using hooks. I ended up with this (simplified):
my Codepen
const App2 = () => {
const [selectedOptions, setSelectedOptions] = React.useState({});
React.useEffect(() => {
console.log(selectedOptions);
},[selectedOptions]);
const updateSelection = (sel) => {
console.log(sel)
setSelectedOptions(sel);
}
return (
<div className="wrapper">
<h1>Toppings</h1>
<OptionsList
options={options}
onChange={updateSelection}
selectedOptions={selectedOptions}
/>
</div>
);
}
const OptionsList = ({ selectedOptions, onChange }) => {
const handleCheckboxClicked = (selectedOptionId) => {
if(selectedOptions[selectedOptionId]){
delete selectedOptions[selectedOptionId];
} else {
selectedOptions[selectedOptionId] = {}
}
onChange(selectedOptions);
}
return (
<div>
<button onClick={() => (handleCheckboxClicked("chicken-id"))} >
Set Chicken
</button>
</div>
)
}
ReactDOM.render(<App2 />, document.querySelector('#app'));
The problem is setSelectedOptions(sel), inside the function updateSelection is not doing absolutely anything (and of course useEffect is not being fired).
I'm not able to figure out why. I put a console.log just above it to check whether the variable ("sel") was okay or not, but it seems fine. I tried hardcoding the value of "sel", {chicken-id: {}}, and it works when I do so.
The problem is that you are directly mutating the state object:
if(selectedOptions[selectedOptionId]){
delete selectedOptions[selectedOptionId];
} else {
selectedOptions[selectedOptionId] = {}
}
onChange(selectedOptions);
In both outcomes you alter the state object and then set it as itself, so naturally the useEffect will not fire as the setter has effected no actual change. You have to be careful of doing this in React as it's an easy way to end up with stale values. Re-renders are only triggered when there is a difference between the current state and the value passed by the setter, not whenever the values of the state change, for whatever reason.
A comment suggested that you use spread - ... - to destructure the state object so that you can set it as itself, but this seems like a bit of an anti-pattern to me especially as it leaves the state mutation in place. If you want to delete an entry from a state object then a general approach would be to clone the object first, mutate the clone, then set that as the new state:
const clone = Object.assign({}, selectedOptions);
if(clone[selectedOptionId]){
delete clone[selectedOptionId];
} else {
clone[selectedOptionId] = {}
};
onChange(clone);
A word of caution though, any nested objects inside this object will retain their references to the original state and could still mutate it. You would have to clone these nested objects as well to avoid the issue.

Resources