converting class based component with state to functional component with hooks - javascript

I have this componet with state, here when the user is online it increases the count. I want to change it to functional component with hooks, which I have done below
class App extends React.Component {
state = {
isOnline: true,
count: 1
}
handleOnline = () => {
if (!this.state.isOnline) {
this.setState({
count: this.state.count + 1
}, () => {
this.setState({ isOnline: !this.state.isOnline })
})
} else {
this.setState({ isOnline: !this.state.isOnline })
}
}
render() {
return (
<div className="App">
<h1>online ==> {this.state.isOnline ? 'Online' : 'Offline'}</h1>
<h1>count ==> {this.state.count}</h1>
<button onClick={this.handleOnline}>Toggle</button>
</div>
);
}
}
Here is my conversion to functional component with hooks,
const App = () => {
const [isOnline, setIsOnline] = useState(true)
const [count, setCount] = useState(1)
const handleClick = () => {
if (!isOnline) {
setIsOnline(!isOnline)
setCount(count + 1)
} else {
setIsOnline(!isOnline)
}
}
return (
<div className="App">
<h1>online ==> {isOnline ? 'Online' : 'Offline'}</h1>
< h1 > count ==> {count}</h1>
<button onClick={handleClick}>Toggle</button>
</div>
)
}
In the class based component, I have read not to use setState one after another so I used the callback function in this.setState like this
this.setState({
count: this.state.count + 1
}, () => {
this.setState({ isOnline: !this.state.isOnline })
})
Now, in the functional component I have used setCount and setIsOnline one after another is it good ???
const handleClick = () => {
if (!isOnline) {
setIsOnline(!isOnline)
setCount(count + 1)
} else {
setIsOnline(!isOnline)
}
I have read to use useEffect for callbacks, but all I get is infinite loop. Even though both of my components work and give me the desired result . I wanted to know if i must use useEffect for the callback or if my implementation with hooks in functional component is correct???

This implementation is correct, yes we should not set one state after the other because setState works asynchronously but since you are only setting two states so its fine.
Although you can also keep one state object instead of both separate states i.e
const [state, setState] = useState({ count: 1, isOnline: true });
And then you can set both object keys in a single setState, like:
setState(() => ({
count: 1,
isOnline: false,
}))
Also in the class based approach you have used a callback but you actually don't need that, you can use single setState for setting both states i.e.
this.setState(() => ({
count: this.state.count + 1,
isOnline: !this.state.isOnline ,
}))
Another important note:
Try to use functional set state as I use in examples above, as it reduces the risk of being caught into React state asynchronous issues.

Calling set state one after the other is totally fine and is the correct thing to do here:
const handleClick = () => {
if (!isOnline) {
setIsOnline(!isOnline)
setCount(count + 1)
} else {
setIsOnline(!isOnline)
}
}
State is updated asychronously, which means that your state variables isOnline and count don't actually change until your component re-renders. Calling setCount and setIsOnline doesn't update these variables, but tells React to update on the next render.
This is why you can't do something like this:
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
}
The count would be incremented by 1, NOT by 2.
Why?
Because the value count has not been updated yet, since we have to wait until re-render before it gets updated. That means count has the same value the entire way through the function - it never changes. So you can call setCount(count + 1) a million times and the value will only ever increment by 1.
This is what people mean when they say you should use the set state callback function.
This is the set state callback function:
const handleClick = () => {
setCount(prev => prev + 1)
setCount(prev => prev + 1)
}
This works as expected, and count will now be incremented by 2.
If we pass a function like this prev => prev + 1 to set state, React will pass the most recent value to the function.
The rule is:
Every time you use the old state value to set the new state value - pass a function to set state.
So although your current implementation does work, you should really be passing a function to set state on count since you are depending on previous state:
const handleClick = () => {
if (!isOnline) {
setIsOnline(!isOnline)
setCount(prev => prev + 1)
} else {
setIsOnline(!isOnline)
}
}
Usually you should do this for your boolean value too, for example:
setIsOnline(prev => !prev)
but since you're using isOnline in your if statement, you shouldn't do that here as the prev value may be different than the value your if uses.

Related

setTimeout for this.state vs useState

When I use class component, I have code:
setTimeout(() => console.log(this.state.count), 5000);
When I use hook:
const [count, setCount] = useState(0);
setTimeout(() => console.log(count), 5000);
If I trigger setTimeout then change the count to 1 before the timeout (5000ms), class component will console.log(1) (the newest value), and for useState it is console.log(0) (value when register timeout).
Why does this happen?
Updated Version:
Question: Difference in behavior of a React State variable inside setTimeout / setInterval for function and class components?
Case 1: State variable in function component (stale closure):
const [value, setValue] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print 0 even after we have changed the state (value)
// Reason: setInterval will create a closure with initial value i.e. 0
console.log(value)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Case 2: State variable in class component (no stale closure):
constructor(props) {
super(props)
this.state = {
value: 0,
}
}
componentDidMount() {
this.id = setInterval(() => {
// It will always print current value from state
// Reason: setInterval will not create closure around "this"
// as "this" is a special object (refernce to instance)
console.log(this.state.value)
}, 1000)
}
Case 3: Let's try to create a stale closure around this
// Attempt 1
componentDidMount() {
const that = this // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// This, too, always print current value from state
// Reason: setInterval could not create closure around "that"
// Conclusion: Oh! that is just a reference to this (attempt failed)
}, 1000)
}
Case 4: Let's again try to create a stale closure in class component
// Attempt 2
componentDidMount() {
const that = { ...this } // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval could create closure around "that"
// Conclusion: It did it because that no longer is a reference to this,
// it is just a new local variable which setInterval can close around
// (attempt successful)
}, 1000)
}
Case 5: Let's again try to create a stale closure in class component
// Attempt 3
componentDidMount() {
const { value } = this.state // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval created closure around value
// Conclusion: It is easy! value is just a local variable so it will be closed
// (attempt successful)
}, 1000)
}
Case 6: Class has won (no extra effort to avoid the stale closure). But, how to avoid it in function component?
// Let's find solution
const value = useRef(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest ref value
// Reason: We used ref which gives us something like an instance field.
// Conclusion: So, using ref is a solution
console.log(value.current)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
source-1, source-2
Case 6: Let's find another solution for function components
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest state value
// Reason: We used updater form of setState (which provides us latest state value)
// Conclusion: So, using updater form of setState is a solution
setValue((prevValue) => {
console.log(prevValue)
return prevValue
})
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Original Version:
The issue is caused by closures and can be fixed by using ref. But here is a workaround to fix it i.e. access the latest state value using "updater" form of setState:
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setTimeout(() => console.log('count after 5 secs: ', count, 'Wrong'), 5000)
}, [])
React.useEffect(() => {
setTimeout(() => {
let count
setCount(p => {
console.log('p: ', p)
count = p
return p
})
console.log('count after 5 secs: ', count, 'Correct')
}, 5000);
}, [])
return (<div>
<button onClick={() => setCount(p => p+1)}>Click me before 5 secs</button>
<div>Latest count: {count}</div>
</div>)
}
ReactDOM.render(<App />, document.getElementById('mydiv'))
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<body>
<div id="mydiv"></div>
</body>
For the useState, it create a timeout using count in the first time. It accesses the count value through a closure. When we set a new value by setCount, the component rerender but does not change the value passed to timeout.We can use const count = useRef(0) and pass to timeout count.current. This will always use the newest value of count.
Check this link for more information.
Timeouts don't play along nicely with reacts declarative programming model. In functional components, each render is a single frame in time. They never change.
When state updates, all state variables are created locally anew and don't overwrite the old closed variables.
You can also think of effects in the same way, where an effect will run in its local realm with all its local state variables on each render and new renders don't affect their output.
The only way to break out of this model is refs. Or class components where state is effectively similar to refs where the instance (this) is the ref container. Refs allow cross-render communication and closure busting. Use sparingly and with caution.
Dan Abramov has a fantastic article explaining all this and a hook that solves this. As you correctly answered, the issue is caused by stale closures. The solution indeed involves using refs.
Explanation
With function components, every render is a function call, creating a new function closure for that specific call. The function component is closing over the setTimeout callback function, so that everything in the setTimeout callback is accessing only the specific render where it was called.
Reusable solution:
Using a Ref and accessing it only within the setTimeout callback will give you a value that is persistent across renders.
However, it isn't that convenient to use a React Ref with a value that is always updating, like a counter. You are in charge of both updating the value, and causing a rerender yourself. Updating a Ref doesn't entail a component render.
My solution, for easy use, is to combine both useState and useRef hooks into a single "useStateAndRef" hook. This way, you get a setter that gets both the value, and a ref for use in async situations such as setTimeout and setInterval:
import { useState, useRef } from "react";
function useStateAndRef(initial) {
const [value, setValue] = useState(initial);
const valueRef = useRef(value);
valueRef.current = value;
return [value, setValue, valueRef];
}
export default function App() {
const [count, setCount, countRef] = useStateAndRef(0);
function logCountAsync() {
setTimeout(() => {
const currentCount = countRef.current;
console.log(`count: ${count}, currentCount: ${currentCount}`);
}, 2000);
}
return (
<div className="App">
<h1>useState with updated value</h1>
<h2>count: {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
<button onClick={logCountAsync}>log count async</button>
</div>
);
}
Working CodeSandbox link: https://codesandbox.io/s/set-timeout-with-hooks-fdngm?file=/src/App.tsx

How does this closure make componentDidUpdate behave like useEffect?

I was reading Dan Abramov's article: A Complete Guide to useEffect, and in the section: Each Render Has Its Own… Everything, there are two examples, first
uses useEffect like this:
const UseEffectCounter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}
The above example logs sequentially after the 3 seconds timeout.
The second example uses componentDidUpdate like this:
class ComponentDidUpdateCounter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({count: this.state.count + 1})}>+1</button>
</div>
)
}
}
If click n times and wait for the timeout finishes, console logs You clicked n times for n times, rather than the sequential number of clicks. This is fine because React mutates this.state.count to always point to the latest value of the state.
However, simply modifying the componentDidUpdate like this would make the component behave like the first example:
componentDidUpdate() {
const count = this.state.count
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
}
How does this closure make the component behave like useEffect?
The reason for the difference is that this.state.count is evaluated at different points in your examples.
this.state.count is evaluated in the third example after each update at the when you assign it to count, whereas in the second example this.state.count is evaluated at the end of the timeout. At the end of the timeout, all of the updates have most likely already occurred, so you see the same values logged. But assigning it to a local count variable only defers the logging, not the evaluation.
In the useEffect version, each render is creating a new count const, and a const can only be assigned once. This is why it logs each value sequentially, because a const can never be updated, even if you wait on a state update to complete.

Regarding prevState callback in .setState()

Case1.
handleRemovePlayer = id => {
this.setState(prevState => {
return {
players: prevState.players.filter(player => player.id !== id)
};
});
};
Case2.
// Arrow Func: become Component instance
incrementScore = () => {
this.setState(prevState => ({
score: this.state.score + 1
}));
};
decrementScore = () => {
if (this.state.score > 0) {
this.setState(prevState => ({
score: this.state.score - 1
}));
}
};
In setState(), why case1 cannot use this.players.filter instead of prevState.player? Both case1 and 2 use the same prevState callback.. Can anyone explain precisely regarding prevState?
Thanks in advance!
Currently, setState() is asynchronous inside event handlers.
Let assume that- you updated your state and you want to check state is updated or not.
for that you can use updater(callback) as 2nd argument to check updated state.
like this -
incrementScore = () => {
this.setState(prevState => ({
score: prevState.score + 1
}),()=>{
console.log(this.state.score)
});
};
Calls to setState are asynchronous - don’t rely on this.state to reflect the new value immediately after calling setState. Pass an updater function instead of an object if you need to compute values based on the current state ...for your reference setState in reactjs
SetState is an asynchronous method. So, if there are more than 1 setState execution methods, the second approach may result in the value which we are not interested in. But the first approach will make sure we always get the latest state value.
One should always use prevState instead of this.state.

React multiple callbacks not updating the local state

I have a child component called First which is implemented below:
function First(props) {
const handleButtonClick = () => {
props.positiveCallback({key: 'positive', value: 'pos'})
props.negativeCallback({key: 'negative', value: '-100'})
}
return (
<div><button onClick={() => handleButtonClick()}>FIRST</button></div>
)
}
And I have App.js component.
function App() {
const [counter, setCounter] = useState({positive: '+', negative: '-'})
const handleCounterCallback = (obj) => {
console.log(obj)
let newCounter = {...counter}
newCounter[obj.key] = obj.value
setCounter(newCounter)
}
const handleDisplayClick = () => {
console.log(counter)
}
return (
<div className="App">
<First positiveCallback = {handleCounterCallback} negativeCallback = {handleCounterCallback} />
<Second negativeCallback = {handleCounterCallback} />
<button onClick={() => handleDisplayClick()}>Display</button>
</div>
);
}
When handleButtonClick is clicked in First component it triggers multiple callbacks but only the last callback updates the state.
In the example:
props.positiveCallback({key: 'positive', value: 'pos'}) // not updated
props.negativeCallback({key: 'negative', value: '-100'}) // updated
Any ideas?
Both are updating the state, your problem is the last one is overwriting the first when you spread the previous state (which isn't updated by the time your accessing it, so you are spreading the initial state). An easy workaround is to split counter into smaller pieces and update them individually
const [positive, setPositive] = useState('+')
const [negative, setNegative] = useState('-')
//This prevents your current code of breaking when accessing counter[key]
const counter = { positive, negative }
const handleCounterCallback = ({ key, value }) => {
key === 'positive' ? setPositive(value) : setNegative(value)
}
You can do that but useState setter is async like this.setState. If you want to base on the previous value you should use setter as function and you can store it in one state - change handleCounterCallback to
const handleCounterCallback = ({key,value}) => {
setCounter(prev=>({...prev, [key]: value}))
}
and that is all. Always if you want to base on the previous state use setter for the state as function.
I recommend you to use another hook rather than useState which is useReducer - I think it will be better for you

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