Why do I need prevProps with prevState in componentDidUpdate()? - javascript

I have built a simple counter app:
class Counter extends React.Component {
constructor(props) {
super(props);
this.handleAddOne = this.handleAddOne.bind(this);
this.handleMinusOne = this.handleMinusOne.bind(this);
this.handleReset = this.handleReset.bind(this);
this.state = {
count: 0
};
}
componentDidMount() {
const stringCount = localStorage.getItem('count');
const count = parseInt(stringCount);
if (isNaN(count) === false) {
this.setState(() => ({ count }));
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
localStorage.setItem('count', this.state.count);
console.log('componentDidUpdate');
}
}
handleAddOne() {
this.setState((prevState) => {
return {
count: prevState.count + 1
}
});
}
handleMinusOne() {
console.log('handleMinusOne');
this.setState((prevState) => {
return {
count: prevState.count - 1
}
});
}
handleReset() {
this.setState(() => {
return {
count: 0
}
});
}
render() {
return (
<div>
<h1>Count: {this.state.count}</h1>
<button onClick={this.handleAddOne}>+</button>
<button onClick={this.handleMinusOne}>-1</button>
<button onClick={this.handleReset}>reset</button>
</div>
);
}
}
ReactDOM.render(<Counter />, document.getElementById('app'));
The question I have is with componentDidUpdate(). In it, I am checking to see if the prevState.count is not the same as the this.state.count. If it is not the same, then I set localStorage to the new count. If it is same, I do nothing.
In the current componentDidUpdate(), I need prevProps as an argument for this function to work correctly. For example, if I just have this:
componentDidUpdate(prevState) {
if (prevState.count !== this.state.count) {
localStorage.setItem('count', this.state.count);
console.log('componentDidUpdate');
}
}
Then the component sets localStorage every time the reset button is pressed repeatedly, even though the count remains at 0.
What is going on? Why do I need prevProps for componentDidUpdate() to work correctly, if I am never using props in that function?

The first parameter in componentDidUpdate is prevProps. The second parameter is prevState. The documentation clearly states that:
componentDidUpdate(prevProps, prevState, snapshot)
This
componentDidUpdate(prevState) {...}
is not a correct signature for the hook. Even though the first parameter was called prevState, it contains previous props.
It's possible to alternate function parameters based on its arity but this isn't implemented in React and considered a bad practice in general because this leads to more complex signatures.
To not cause linter warnings, unused parameters can be underscored by convention:
componentDidUpdate(_prevProps, prevState) {...}

It doesn't matter if you are using prevProps in your function or not, it has to be included, since the arguments order in functions does matter.
componentDidUpdate(prevProps, prevState)
If you'd omit the prevProps argument, the prevState (even tho the name would say clearly that it's previous version of state) it would stand for the previous version of props.
It's just a blank. You can use either an underscore, to tell others (or just yourself) that it's not used in that function.
componentDidUpdate(_, prevState)

Because componentDidUpdate receives prevProps as the first argument and prevState as second. If you write the function as componentDidUpdate(prevState) it is receiving prevProps but it will be named prevState. No matter how you name the arguments, they will be prevProps, prevState, and snapshot, respectively.
If you don't need the first argument, you can use an underscore eg. componentDidUpdate(_, prevState).

Related

React use setState to update object but not re-render

I want to use setState to update the object in state which called 'all_cart",
but not matter which method I tryed, it cannot trigger re-render, I know React will use === to check two object in state is changed or not.
Here is my code:
this.setState({
all_cart: {
...this.state.all_cart,
cart_data : _response['data'].data.cart_data
}
})
this.setState(({all_cart}) => ({
all_cart: {
...this.state.all_cart,
cart_data : _response['data'].data.cart_data
}
}))
How can I do?
According to the React docs if you have props, it is wrong approach:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
To fix it, use a second form of setState() that accepts a function
rather than an object. That function will receive the previous state
as the first argument, and the props at the time the update is applied
as the second argument:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
And your code would look like this:
this.setState((state, props) => ({
all_cart: {
...state.all_cart,
cart_data : _response['data'].data.cart_data
}
}))
UPDATE:
However, if you do not use props, then it looks like your function is not is scope, so try to use arrow function. Let me show an example:
handleClick = () => {
this.setState(this.someApiResult())
}
So a complete stackblitz example can be seen here.
As per my understanding, you want to merge _response['data'].data.cart_data with the existing state which is all_cart,
in this case, you should try this
function updateCart(){
const updatedData = {...this.state.all_cart, ..._response['data'].data.cart_data};
this.setState({all_cart: updatedData});
}

Pass params from state to a checkbox tree

I have this code :
constructor(props) {
super(props);
this.dispatch = props.dispatch;
this.state = {
checked: [],
expanded: [],
};
const {dispatch, match, t} = this.props;
this.onCheck = this.onCheck.bind(this);
this.onExpand = this.onExpand.bind(this);
}
onCheck(checked) {
this.setState({ checked });
console.log(this.state.checked);
this.loadProducts(this.state.checked);
}
onExpand(expanded) {
this.setState({ expanded });
}
render() {
const { checked, expanded } = this.state;
console.log(checked);
........
<CheckboxTree
nodes={this.props.chosenCategory.children}
checked={checked}
expanded={expanded}
onCheck={this.onCheck}
onExpand={this.onExpand}
.......
/>
The problem is that in console.log from render function I have the correct checked value. But in the function onCheck checked have the previous value. I don't understand what's the problem. Can you help me please ?
Due to the asynchronous behaviour of setState, we can't guarantee that the following line will have the updated value, at that particular instance.
In your case, if you want to log the updated value inside onCheck, the you can use the callback provided by the setState function.
onCheck(checked) {
this.setState({ checked }, () => {
this.loadProducts(this.state.checked); //param - checked will also do
console.log(this.state.checked);
});
}
now the console.log will execute, after successful setState.
There are 2 types of setState, the first one takes an object as a parameter, and the second one takes a function as a parameter. The difference between the two is the first one is asynchronous and the second is synchronous.
So you need to use setState with function as a parameter.
onCheck(checked) {
this.setState((state) => ({ checked}));
console.log(this.state.checked);
this.loadProducts(this.state.checked);
}

react native state TypeError: One of the sources for assignhas an enumerable key on the prototype chain

I'm having trouble understanding state and setState and can't get my code to work:
constructor:
constructor(){
super();
this.state = {
Ingredient : getRandomIngredient(baseArray)
}
}
button:
<Button title="Mix Salad"
onPress = {()=>this.handleButtonPress()}
/>
function:
handleButtonPress= () => {
this.setState(
this.Ingredient = getRandomIngredient(baseArray)
)
}
The error occurs once i press the button.
Question a) How do I use state and setState correctly?
Question b) The initial state doesn't persist through the render() function. My Ingredient object remains undefined. That probably isn't intended. What can I do that the Ingredient object retains it's values?
this is wrong syntax
this.setState(
this.Ingredient = getRandomIngredient(baseArray)
)
correct :
this.setState({ Ingredient : getRandomIngredient(baseArray) })
SetState function take ALWAYS and object parameter
EDit :
take care about async function, if getRandomIngredient() take time to return value so Ingredient will be null
You need to use an object in your setState.
handleButtonPress = () => {
this.setState({
Ingredient: getRandomIngredient(baseArray)
});
}

React setState re-render

First of all, I'm really new into React, so forgive my lack of knowledge about the subject.
As far as I know, when you setState a new value, it renders again the view (or parts of it that needs re-render).
I've got something like this, and I would like to know if it's a good practice or not, how could I solve this kind of issues to improve, etc.
class MyComponent extends Component {
constructor(props) {
super(props)
this.state = {
key: value
}
this.functionRender = this.functionRender.bind(this)
this.changeValue = this.changeValue.bind(this)
}
functionRender = () => {
if(someParams !== null) {
return <AnotherComponent param={this.state.key} />
}
else {
return "<span>Loading</span>"
}
}
changeValue = (newValue) => {
this.setState({
key: newValue
})
}
render() {
return (<div>... {this.functionRender()} ... <span onClick={() => this.changeValue(otherValue)}>Click me</span></div>)
}
}
Another component
class AnotherComponent extends Component {
constructor (props) {
super(props)
}
render () {
return (
if (this.props.param === someOptions) {
return <div>Options 1</div>
} else {
return <div>Options 2</div>
}
)
}
}
The intention of the code is that when I click on the span it will change the key of the state, and then the component <AnotherComponent /> should change because of its parameter.
I assured that when I make the setState, on the callback I throw a console log with the new value, and it's setted correctly, but the AnotherComponent doesn't updates, because depending on the param given it shows one thing or another.
Maybe I need to use some lifecycle of the MyComponent?
Edit
I found that the param that AnotherComponent is receiving it does not changes, it's always the same one.
I would suggest that you'll first test it in the parent using a simple console.log on your changeValue function:
changeValue = (newValue) => {
console.log('newValue before', newValue);
this.setState({
key: newValue
}, ()=> console.log('newValue after', this.state.key))
}
setState can accept a callback that will be invoked after the state actually changed (remember that setState is async).
Since we can't see the entire component it's hard to understand what actually goes on there.
I suspect that the newValue parameter is always the same but i can't be sure.
It seems like you're missing the props in AnotherComponent's constructor. it should be:
constructor (props) {
super(props) // here
}
Try replacing the if statement with:
{this.props.param === someOptions? <div>Options 1</div>: <div>Options 2</div>}
also add this function to see if the new props actually get to the component:
componentWillReceiveProps(newProps){
console.log(newProps);
}
and check for the type of param and someOptions since you're (rightfully) using the === comparison.
First, fat arrow ( => ) autobind methods so you do not need to bind it in the constructor, second re-renders occur if you change the key of the component.
Ref: https://reactjs.org/docs/lists-and-keys.html#keys

Is it okay to treat the prevState argument of setState's function as mutable?

I know this.state is not supposed to be modified directly, instead setState should be used.
From this I inferred that prevState should be also treated as immutable and instead setState should always look something like this:
this.setState((prevState) => {
// Create a new object instance to return
const state = { ...prevState };
state.counter = state.counter + 1;
return state;
});
Or with deeper nesting:
this.setState((prevState) => {
// Create a new object instance to return
const state = { ...prevState, counter: { ...prevState.counter } };
state.counter.value = state.counter.value + 1;
return state;
});
Or just a partial update like would be with setState({}) where easier and nicer to use:
this.setState((prevState) => ({ counter: prevState.counter + 1 }));
All of the above are obviously correct because they return a new instance, but then I came across this question where the accepted answer encourages mutating prevState without returning a new object instance (notice the code block in the question).
Something like this:
this.setState((prevState) => {
prevState.flag = !prevState.flag;
return prevState;
});
I found this to be a sketchy suggestion so I decided to test if the object instance references of prevState and this.state are the same:
(The JSFiddle)
class Test extends React.Component {
state = { counter: 0 };
onDemonstrateButtonClick = (event) => {
this.setState((prevState) => {
if (prevState === this.state) alert(`uh, yep`);
prevState.counter++;
return prevState;
});
};
render() {
return (
<div>
<button onClick={this.onDemonstrateButtonClick}>Demonstrate</button>
{this.state.counter}
</div>
);
}
}
Whadayaknow, they are! So which is it? Is the answer wrong and should I return a new object instance or return partial update as a new object or can I go wild and mutate the prevState argument directly? And if yes, how is this any different from mutating this.state directly?
Side note: TypeScript React typings do not mark the argument as ReadOnly which only adds to my confusion.
First Point
Is it okay to treat the prevState argument of setState's function as
mutable?
The answer is NO you should never mutate prevState, this is also clearly mentioned in react documentation in setState section
prevState is a reference to the previous state. It should not be
directly mutated. Instead, changes should be represented by building a
new object based on the input from prevState and props.
Second Point:
You tested prevState and this.state and they are the same, well actually they are not.
To figure out why they are actually different we need to know why prevState actually exist, and the answer is that setState function is asynchronous, thats why react js is giving us access to prevState lets check the example below where prevState != this.state
In the example below we will increment counter twice per click, but we will use 2 setState operations each one of them will increment the counter by 1.
Because setState is async you will notice that the second setState operation started before the first setState is finished this is where prevState is useful and here prevState and this.state are not equal.
I commented each line with a number denoting when this line is executed, this should explain why we need prevState and why it is different from this.state.
class App extends React.Component{
constructor(props)
{
super(props);
this.state = {
counter : 1
};
}
increment = () =>{
this.setState((prevState , props) => {
console.log("in first"); //3
console.log(prevState); //3
console.log(this.state); //3
if(prevState == this.state)
{
console.log("in first prevState == this.state");
}
return {
counter : prevState.counter+1
}
} , ()=>{
console.log("done updating first"); //5
});
console.log("after first"); //1
this.setState((prevState, props) => {
console.log("in second"); //4
console.log(this.state); //4
console.log(prevState); //4
if (prevState == this.state) {
console.log("in second prevState == this.state");
}
return {
counter: prevState.counter + 1
}
} , () =>{
console.log("done updating second"); //6
});
console.log("after second"); //2
}
render(){
return (
<div>
<span>{this.state.counter}</span>
<br/>
<button onClick={this.increment} >increment</button>
</div>
)
}
}
The Result from the above code is
"after first"
"after second"
"in first"
▶Object {counter: 1}
▶Object {counter: 1}
"in first prevState == this.state"
"in second"
▶Object {counter: 1}
▶Object {counter: 2}
"done updating first"
"done updating second"
The above code is fully working in this link, you can check the console.log result
https://codesandbox.io/s/k325l485mr
The above example will correctly increment counter twice per click, if you want to break it change return statement in second setState
from
return {
counter: prevState.counter + 1
}
to
return {
counter: this.state.counter + 1
}
and you will find that the result is not correct each click will result in 1 increment which is not correct because we have 2 setState , this is because we didn't use prevState and we used an incorrect this.state
Finally
I believe that the correct way to update the counter is
this.setState((prevState) => ({ counter: prevState.counter + 1 }));

Categories

Resources