understanding useCallback and setState - reactjs

I have some simple lines of code to test the useCallback and setState hook. First I try to write like this:
const App = () => {
const [count, setCount] = useState(0)
const increase = useCallback(() => {
setCount(count + 1) // Passing value to change state
}, [])
console.log(count)
return (
<div id="main">
<h1>Count: {count}</h1>
<button onClick={increase}>Increase</button>
</div>
)
}
export default App;
and then, passing the callback to setCount instead like this:
const App = () => {
const [count, setCount] = useState(0)
const increase = useCallback(() => {
setCount(count => count + 1) // Passing callback
}, [])
console.log(count)
return (
<div id="main">
<h1>Count: {count}</h1>
<button onClick={increase}>Increase</button>
</div>
)
}
export default App;
I know it is weird to use useCallback in this situation, just for some testing. The question is that why I pass the callback to setCount, the Increase button work correctly, whereas the first one will just re-render on the first click

In the first case useCallback closes over the count and at that time count was 0 so It always changes value to 1
So to make it working you should add count dependency to useCallback as:
CODESANDBOX LINK
const increase = useCallback(() => {
setCount(count + 1); // Passing value to change state
}, [count]);
In the second case it is not taking the value from the closure instead it is taking the current value and adds one to it and set the value.
ADDITIONAL INFO
You can use react-hooks/exhaustive-deps eslint rule to know which dependency is missing and tell you how to correct it.

useCallback() hasn't been used correctly here: the dependencies array should have count in it. So:
const increase = useCallback(() => {
setCount(count + 1) // Passing value to change state
}, [count])
With an empty array as a dependency, it will be memoized for the lifetime of the component and will not update the current count value.
Source: https://reactjs.org/docs/hooks-reference.html#usecallback
Code sandbox
On a side note, using useCallback() is "overoptimizing" in this case, we are complicating the code while the performance impact to the end users is not noticeable. I would only use it to prevent rerenders in complex views e.g. in a huge list.

Related

Why my State values cannot be updated with a timer in react?

Im new in react and Im doing some basic stuff... so, I started using the states and I found it quite useful, however, there is one thing that doesnt make sense in my mind.
It all started by having a time displayed, something like this
function MyAPp() {
const initValue = new Date().toLocaleTimeString();
const [value, setCount] = React.useState(initValue);
function updateValue() {
setCount(new Date().toLocaleTimeString())
}
setInterval(updateValue, 1000)
return <div>
<label> The value of my state is { value } </label>
</div>
So, the above works great, updates the time every second and nothing wrong with it...
Now when I do this:
function MyApp() {
const [count, setCount] = React.useState(123);
function updateValue() {
setCount(count + 1)
}
setInterval(updateValue, 1000)
return <div>
<label> The value of my state is { count } </label>
</div>
}
When it reaches 8 or 9, starts to have a weird behaviour, it starts to get updated less than every second, and the update goes jumping from number to number, like from 8 to 9 and then back to 8, then quickly back to 9 and 10, 11 and back to 9 or 8 again and it just gets crazy....
Checking about the limitations of getting the value and use it to set the count, makes no sense why is failing this way.
And even worse, why the interval seems to be affected, feels like it would be getting into a loop by calling MyApp() or something like that, which is not happening...
Any idea what could be wrong with this?
There are 2 issues in the above-mentioned implementation.
setInterval is being called on every render.
This can be solved by calling this inside useEffect without any dependencies. It'll make sure the interval is being called only once.
This is a case of stale props or state in react hook. For more refer the official documentation.
The setter method of state accepts a callback. It gives the current state in the callback which will always have the final update value.
Kent Dodds mentions this as a Lazy initialization of state. Here's a really good blog on the same.
Here's the working example solving this issue at codesandbox
Code
import { useState, useEffect } from "react";
export default function App() {
const [count, setCount] = useState(123);
useEffect(() => {
setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
}, []);
return (
<div>
<label> The value of my state is {count} </label>
</div>
);
}
becuase setInterval is a method that calls a function or runs some code after specific intervals of time, as specified through the second parameter.
clearInterval is a function or block of code that is bound to an interval executes until it is stopped. After the React component unmounts the interval is cleared.
by two methods , your scheduling works good in display count.
export default function App() {
const [count, setCount] = React.useState(123);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, [count]);
return (
<div>
<label> The value of my state is {count} </label>
</div>
);
}
You are doing it absolutely wrong, firstly as you can see your setInterval is registered on every render which is causing the unexpected behavior and secondly it's always recommended to merge the old and new state when using setInterval as the value will remain same in the setInterval callback.
Here is the solution to your problem.
function MyApp() {
const [count, setCount] = React.useState(123);
const updateValue = React.useCallback(() => {
setCount((old) => old + 1);
}, []);
React.useEffect(() => {
setInterval(updateValue, 1000);
}, [updateValue]);
return (
<div>
<label> The value of my state is {count} </label>
</div>
);
}
So in this solution we have used React.useEffect which has only one dependency which means when the dependency will be updated the call of React.useEffect will be invoked but in this case it will only be invoked once the component is rendered.
Checkout this demo

useState() undefined in React just after set

I have a problem and I do not undersatnd why the hook return undefined :
import React, { useEffect, useState } from 'react';
function App(){
const [globalVariable, setGlobalVariable] = useState();
useEffect(()=>{
const test = 5
console.log(test) // return 5
setGlobalVariable(test)
console.log(globalVariable) // return undefined
},[]);
return (
<div>
</div>
);
}
export default App;
How can I do in order to set directly a new value for the globalVariable ?
As mentioned by other members, setState runs asynchronously in a queue-based system. This means that if you want to perform a state update, this update will be piled up on the queue. However, it does not return a Promise, so you can't use a .then or a wait. Because of this, we need to be aware of possible problems that might occur while manipulating states in React.
Say we have:
// PROBLEM # 1
export function Button() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
console.log(count); // returns 0
}
return (
<button onClick={increment}>Increment<\button>
)
}
That is basically the same problem you have. Since setCount is asynchronous, the console.log will return the value before the updates (which is 0 here). In your case, it will return undefined because you didn't pass any initial state in the useState hook. Other thing you did different was to fire the state change from inside the useEffect hook, but that doesn't matter for the problem at hand.
To fix this, a good practice is to store the new state in another variable, as follows:
// SOLUTION TO PROBLEM # 1
export function Button() {
const [count, setCount] = useState(0);
function increment() {
const newCountValue = count + 1;
setCount(newCountValue);
console.log(newCountValue); // returns 1
}
return (
<button onClick={increment}>Increment<\button>
)
}
This basically answers your question.
But, there are still other scenarios where state updates might not behave as one would expect. It is important to be aware of these scenarios to avoid possible bugs. For example, let's assume that we are updating a given state multiple times in a row, like:
// PROBLEM # 2
export function Button() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
console.log(count); // returns 0, but the final state will be 1 (not 3)
}
return (
<button onClick={increment}>Increment<\button>
)
}
Here, console.log still returns 0 for the same reason described in Problem #1. But, if you are using the count state somewhere in your app, you will see that the final value after the onClick event is 1, not 3.
Here is how you can fix this:
// SOLUTION TO PROBLEM # 2
export function Button() {
const [count, setCount] = useState(0);
function increment() {
setCount((oldState) => oldState + 1);
setCount((oldState) => oldState + 1);
setCount((oldState) => oldState + 1);
console.log(count); // returns 0, but the final state will be 3
}
return (
<button onClick={increment}>Increment<\button>
)
}
By passing a function to setState, the new state is computed using the previous value on the queue and not the initial one.
As a final example, say we are calling a function right after trying to mutate our state, but this function depends on the state value. Since the operation is asynchronous, that function will use the "old" state value, just like in Problem #1:
// PROBLEM # 3
export function Button() {
const [count, setCount] = useState(0);
onCountChange() {
console.log(count); // returns 0;
}
function increment() {
setCount(count + 1);
onCountChange();
}
return (
<button onClick={increment}>Increment<\button>
)
}
Notice in this problem that we have a function onCountChange that depends on an external variable (count). By external I mean that this variable was not declared inside the scope of onCountChange. They are actually siblings (both lie inside the Button scope). Now, if you are familiar with the concept of "pure functions", "idempotent" and "side effects", you will realize that what we have here is a side-effect issue -- we want something to happen after a state change.
Roughly speaking, whenever you need to handle some side-effects in React, this is done via the useEffect hook. So, to fix our 3rd problem, we can simply do:
// SOLUTION TO PROBLEM # 3
export function Button() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count); // returns 0;
}, [count]);
function increment() {
setCount(count + 1);
}
return (
<button onClick={increment}>Increment<\button>
)
}
In the solution above, we moved the content of the onCountChange to useEffect and we added count as a dependency, so whenever it changes, the effect will be triggered.
Because state only has new value when component re-render. So just put console.log(globalVariable) outside useEffect like this:
useEffect(() => {
const test = 5;
console.log(test); // return 5
setGlobalVariable(test);
}, []);
console.log(globalVariable);
Two things:
useState has an initial value, defaulted to undefined.
const [globalVariable, setGlobalVariable] = useState(123); // initial value set to 123
setGlobalVariable is not a synchronous operation. Changing it will not mutate globalVariable immediately. The value in scope will remain the same until the next render phase.
This is because component is not re-rendering. You need to keep the value between the renders and for that you need to change the variable to a state using useState. Check below code for better understanding:
import React, { useEffect, useState } from "react";
function Test() {
const [globalVariable, setGlobalVariable] = useState();
const [test, setTest] = useState(5);
useEffect(() => {
// const test = 5;
// console.log(test); // return 5
setGlobalVariable(test);
console.log(globalVariable); // return undefined
console.log(test); // return 5
}, []);
return <div></div>;
}
export default Test;
This is because React state update is queue based system. It won't update immediately. But if you render a global variable value, you get the required one.
If you have same requirement in class-based component, then you need to pass callback function to setState. It will execute as soon as state is updated.
this.setState(stateupdate, callback)
Once the state is updated, the callback function will execute with the latest state.
const StackOverflow = () => {
const [globalVariable, setGlobalVariable] = useState();
useEffect(()=>{
const test = 5
console.log(test) // return 5
setGlobalVariable(test)
console.log(globalVariable)
},[]);
return (
<Container maxWidth="lg" style={{paddingTop:"5%"}}>
<div>
<h1>StackOverflow - {globalVariable}</h1>
</div>
</Container>
)
}
This is well-explained in the official documentation, §caveats:
The set function only updates the state variable for the next render. If you read the state variable after calling the set function, you will still get the old value that was on the screen before your call.
And especially in §troubleshooting: "I’ve updated the state, but logging gives me the old value"
that's because the setGlobalVariable(test); takes some time, and you are trying to do it in the useEffect, which runs only once, you should use a setTimeOut function to take some time to show the state, try this and let me know is it working or not.
const [globalVariable, setGlobalVariable] = useState();
useEffect(() => {
const test = 5;
// console.log(test); // return 5
setGlobalVariable(test);
setTimeout(() => {
console.log(globalVariable);
}, 2000);
}, []);

How to deal with stale state values inside of a useEffect closure?

The following example is of a Timer component that has a button (to start the timer), and two tags that display the number of elapsed seconds, and the number of elapsed seconds times 2.
However, it does not work (CodeSandbox Demo)
The Code
import React, { useState, useEffect } from "react";
const Timer = () => {
const [doubleSeconds, setDoubleSeconds] = useState(0);
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
console.log("Creating Interval");
setSeconds((prev) => prev + 1);
setDoubleSeconds(seconds * 2);
}, 1000);
} else {
clearInterval(interval);
}
return () => {
console.log("Destroying Interval");
clearInterval(interval);
};
}, [isActive]);
return (
<div className="app">
<button onClick={() => setIsActive((prev) => !prev)} type="button">
{isActive ? "Pause Timer" : "Play Timer"}
</button>
<h3>Seconds: {seconds}</h3>
<h3>Seconds x2: {doubleSeconds}</h3>
</div>
);
};
export { Timer as default };
The Problem
Inside the useEffect call, the "seconds" value will always be equal to the its value when the useEffect block was last rendered (when isActive last changed). This will result in the setDoubleSeconds(seconds * 2) statement to fail. The React Hooks ESLint plugin gives me a warning regarding this problem that reads:
React Hook useEffect has a missing dependency: 'seconds'. Either include it or remove the dependency array. You can also replace
multiple useState variables with useReducer if 'setDoubleSeconds'
needs the current value of 'seconds'.
(react-hooks/exhaustive-deps)eslint
And correctly so, adding "seconds" to the dependency array (and changing setDoubleSeconds(seconds * 2) to setDoubleSeconds((seconds + 1) * ) will render the correct results. However, this has a nasty side effect of causing the interval to be created and destroyed on every render (the console.log("Destroying Interval") fires on every render).
So now I am looking at the other recommendation from the ESLint warning "You can also replace multiple useState variables with useReducer if 'setDoubleSeconds' needs the current value of 'seconds'".
I do not understand this recommendation. If I create a reducer and use it like so:
import React, { useState, useEffect, useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "SET": {
return action.seconds;
}
default: {
return state;
}
}
};
const Timer = () => {
const [doubleSeconds, dispatch] = useReducer(reducer, 0);
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
console.log("Creating Interval");
setSeconds((prev) => prev + 1);
dispatch({ type: "SET", seconds });
}, 1000);
} else {
clearInterval(interval);
}
return () => {
console.log("Destroying Interval");
clearInterval(interval);
};
}, [isActive]);
return (
<div className="app">
<button onClick={() => setIsActive((prev) => !prev)} type="button">
{isActive ? "Pause Timer" : "Play Timer"}
</button>
<h3>Seconds: {seconds}</h3>
<h3>Seconds x2: {doubleSeconds}</h3>
</div>
);
};
export { Timer as default };
The problem of stale values will still exist (CodeSandbox Demo (using Reducers)).
The Question(s)
So what is the recommendation for this scenario? Do I take the performance hit and simply add "seconds" to the dependency array? Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there? Do I merge "seconds" and "doubleSeconds" into a single state object? Do I use refs?
Also, you might be thinking "Why don't you simply change <h3>Seconds x2: {doubleSeconds}</h3>" to <h3>Seconds x2: {seconds * 2}</h3> and remove the 'doubleSeconds' state?". In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable.
Thanks!
You can access a value inside an effect callback without adding it as a dep in a few ways.
setState. You can tap the up-to-date value of a state variable through its setter.
setSeconds(seconds => (setDoubleSeconds(seconds * 2), seconds));
Ref. You can pass a ref as a dependency and it'll never change. You need to manually keep it up to date, though.
const secondsRef = useRef(0);
const [seconds, setSeconds] = useReducer((_state, action) => (secondsRef.current = action), 0);
You can then use secondsRef.current to access seconds in a block of code without having it trigger deps changes.
setDoubleSeconds(secondsRef.current * 2);
In my opinion you should never omit a dependency from the deps array. Use a hack like the above to make sure your values are up-to-date if you need the deps not to change.
Always first consider if there's some more elegant way to write your code than hacking a value into a callback. In your example doubleSeconds can be expressed as a derivative of seconds.
const [seconds, setSeconds] = useState(0);
const doubleSeconds = seconds * 2;
Sometimes applications aren't that simple so you may need to use the hacks described above.
Do I take the performance hit and simply add "seconds" to the dependency array?
Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there?
Do I merge "seconds" and "doubleSeconds" into a single state object?
Do I use refs?
All of them work correctly, although personally I would rather choose the second approach:
useEffect(() => {
setDoubleSeconds(seconds * 2);
}, [seconds]);
However:
In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable
That is questionable. Child component might be implemented like the following:
const Child = ({second}) => (
<p>Seconds: {second}s</p>
);
And parent component should look like the following:
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// change seconds
}, []);
return (
<React.Fragment>
<Child seconds={second} />
<Child seconds={second * 2} />
</React.Fragment>
);
This would be a more clear and concise way.

Handling out of date state in functional components

I am running into issues setting a state created with the 'useState' hook from within async functions.
I've created a codepen to demonstrate: https://codepen.io/james-ohalloran/pen/ZdNwWQ
const Counter = () => {
const [count, setCount] = useState(0);
const increase = () => {
setTimeout(() => {
setCount(count + 1);
},1000);
}
const decrease = () => {
setTimeout(() => {
setCount(count - 1);
},1000)
};
return (
<div className="wrapper">
<button onClick={decrease}>-</button>
<span className="count">{count}</span>
<button onClick={increase}>+</button>
</div>
);
};
In the above example, if you click 'increase' followed by 'decrease'..you will end up with -1 (I would expect it to be 0).
If this was a React class instead of a functional component, I would assume the solution would be to use bind(this) on the function, but I didn't expect this to be an issue with arrow functions.
It is because of using setTimeout
Let's assume that you've called the increase() 10 times in a second.
count will be always 0. Because the state is updated after a second, every increment() called in a second will have an unupdated count.
So every increment() will call setCount(0 + 1);.
So no matter how many times you call in a second, the count is always 1.
Ah, I found a solution. I didn't realize I'm able to reference the previousState from the useState setter function: https://reactjs.org/docs/hooks-reference.html#functional-updates

useState hook: update a state based on another just updated state

I'm currently looking in to React's funtion Component and the useState hook.
const [stateA, setStateA] = useState(somevalue)
const [stateB, setStateB] = useState(somevalue)
function onClick(value){
setStateA(stateA => transformedValue) //a is now transformedValue
//Use transformedValue to update state B
setStateB(...)
}
My question is how to use the newly updated stateA(transformedValue) to updateB after that.
So far i could only access the previous value of State A, not the transformedValue.
I know i could calculate transformedValue outside of the setStateA function, then use that to setStateB. But is it the correct way?
I know can also merge stateA and stateB into one State too.
What is the correct approach? Is there another more preferred way?
The best approach is using useEffect as mentioned.
useEffect(() => {
setCounter2(count + 1);
}, [count]);
Because of you obsessed with rendering once, try this:
function App() {
const [count, setCount] = useState(0);
const [dummy, setDummy] = useState(0);
useEffect(() => {
console.log(`Renderderd ${count} times`);
});
function handleClick() {
setCount(prevCounter => {
setDummy(prevCounter + 1);
return prevCounter + 1;
});
}
return (
<>
<div>You clicked count {count} times!</div>
<div>Dummy= {dummy} !</div>
<button onClick={handleClick}>Click me for rendering!</button>
</>
);
}
As the method setState is asynchronous.
Your stateA won't update immediately when setState is called.
A way to implement this idea is that
to "watch" a stateA by using useEffect method to ensure stateA is updated.
code seems like this:
useEffect(()=>{
setStateB(stateA); // or do anything with updated stateA
},[stateA]);

Resources