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

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

Related

state data not changing in the render/display

I want to change the property amount in a state object using buttons (increment and decrement). I checked using console.log and the property's value is changing when the buttons are clicked, but the displayed number is not changing. why is that? what am I doing wrong?
here's my code: (codesandbox)
import React, { useState, useEffect } from "react";
import { Button } from "react-bootstrap";
export default function App() {
const [data, setData] = useState({});
useEffect(() => {
const temp = {
id: 1,
name: "apple",
amount: 10
};
setData(temp);
}, []);
const handleInc = () => {
let temp = data;
temp.amount++;
console.log("increment", temp.amount);
setData(temp);
};
const handleDec = () => {
let temp = data;
temp.amount--;
console.log("decrement", temp.amount);
setData(temp);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<label>name: {data.name}</label>
<br />
<label>amount: {data.amount}</label>
<br />
<Button onClick={handleDec}>Reduce</Button>
<Button onClick={handleInc}>Increase</Button>
</div>
);
}
let temp = data;
temp.amount++;
setData(temp);
does not update data, as temp == data even after temp.amount++.
The state setter accepts either a new object or a state update callback.
Since you are updating state using it's old value, you need a state update callback,
that returns a new object (via cloning).
setData((data)=> {
let temp = {...data}; // or Object.assign({}, data);
temp.amount++;
return temp;
}
Likewise, for decrementing.
See https://beta.reactjs.org/learn/updating-objects-in-state
and https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
You have 2 issues here. First one is that you using object as your state and object is reference type. When you do let temp = data you just referecing the same exact "data" object using different variable (pointer) "temp". Simply speaking once you change property in one variable, it "changes" the other. Now that also means that whatever you do, temp will always be equal to data cause they are referencing the same object. So to React state it means that it never really changes in your case, when you do setState you passing the same exact reference, so React sees that nothing changed - so it doesn't trigger re-render. Hope it is clear.
Fix in this case is to create a copy of object, in your case it could be simply setState({...temp})
The second issue in your case is that you are not using functional setState, which in your case is needed. The way you wrote it, might lead to bugs and unexpected behaviours, basically whenever you need to modify the state based on previous state value - you need to use functional setState. There are a lot of topics on this, let me reference just one - https://www.freecodecamp.org/news/functional-setstate-is-the-future-of-react-374f30401b6b/
In your case correct solution would be setState((prevState) => ({...prevState, amount: prevState.amount + 1}))
I think you should use Callback with useState to resolve this bug.
const handleInc = () => {
setData((prevState) => ({ ...prevState, amount: prevState.amount + 1 }));
};
const handleDec = () => {
setData((prevState) => ({ ...prevState, amount: prevState.amount - 1 }));
};
Take note of both of the other answers by Nice Books and Nikita Chayka they both touch on important topics that will help you avoid this issue in the future. If you want to update an object using states you need to reconstruct the entire object when you reset the object. I made a fork of your sandbox you can take a look at of a working example that should solve your issue Forked sandbox.
Also the docs reference this issue as well Doc reference.
Please let me know if you need any additional information.

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);
};

Why component in react dont re-rendering

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.

Updating one State with two fields?

I am trying to update my state data based on the users input in two fields and I'm not sure if Im going about it the right way.
The parent component Encounter.js holds the state I will try and limit the amount of code I add here so my issue is clear. So in ComponentDidUpdate I set the state with an object and create an update function to update the state. I pass the two values inside my state to another component PatientInfo along with the update state function:
componentDidUpdate(prevProps) {
if (this.props.details && this.props.details.medicalIntake && !prevProps.details.medicalIntake) {
this.setState({ pertinentMedications: {
covid19Protocol: this.props.details.medicalIntake.pertinentMedications.covid19Protocol,
note: "" || this.props.details.medicalIntake.pertinentMedications.notes
}})
}
}
pertinentMedicationsChange = (newValues) => {
this.props.setIdleTime();
this.props.setState({pertinentMedications: newValues});
}
return (
<PatientInfo
covid19Protocol={this.state.pertinentMedications.covid19Protocol}
pertinentMedicationsNote={this.state.pertinentMedications.note}
pertinentMedicationsChange={this.pertinentMedicationsChange}
/>
)
PatientInfo.js simply passes the props down.
<PertinentMedications
covid19Protocol={this.props.covid19Protocol}
pertinentMedicationsNote={this.props.pertinentMdicationsNote}
pertinentMedicationsChange={this.props.pertinentMedicationsChange}
/>
PertinentMedications.js is where the user input will be collected:
const PertinentMedications = ({
covid19Protocol,
pertinentMedicationsNote,
pertinentMedicationsChange
}) => {
const [isChecked, setIsChecked] = useState(covid19Protocol)
const onClick = (field, value) => {
setIsChecked(!isChecked)
pertinentMedicationsChange( {[field]: value})
}
const onNoteChange = (field, value) => {
pertinentMedicationsChange( {[field]: value})
}
return(
<ContentBlock title="Pertinent Medications and Supplements">
<CheckToggle onChange={() => onClick("covid19Protocol", !covid19Protocol)} checked={isChecked}>
<p>Patient has been receiving the standard supportive care and supplements as per COVID-19 protocol.</p>
</CheckToggle>
<Input
type="textarea"
name="pertinentMedications"
onChange={e => onNoteChange("notes" ,e.target.value)}
value={pertinentMedicationsNote}
/>
</ContentBlock>
)
}
export default PertinentMedications;
My true question lies within the pertinentMedicationsChange function as Im not sure how to take the data im getting from the PertinentMedications component and format it to be placed in the state. First Im not sure if I can update the state the way im trying to with these two independent fields that send their data to this function to change the state? And If it is possible Im not sure how to properly setup the key value pairs when i call setState. Can anyone help?
it seems that you are calling this.props.setState instead of this.setState. Second, this.setState also accepts a function which first param is the previous state. In this way you can use it to prevent its key values saved from pertinentMedications to be overwritten. fwiw, it's better to be consistent, not mixing hooks with react component based.
pertinentMedicationsChange = (newValues) => {
this.props.setIdleTime();
this.setState((state) => ({
// you create a new object with previous values, while newValues updates the proper keys, but not removing other keys
pertinentMedications: { ...state.pertinentMedications,...newValues}
});
)};

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