React, useCallback re-call again even if dependency did not change - reactjs

I'm using useCallback to memoize a function, I wrote 2 functions, formatCounter and formatCounter2 and added useCallback to the functions with dependencies of counter and counter2.
When onClick function is called, the counter variable has changed but formatCounter2 called, but why? As I understand it needs to be called only when the dependency changes (counter2) and it does not change.
function App() {
const [counter, setCounter] = React.useState(0);
const [counter2, setCounter2] = React.useState(0);
const onClick = React.useCallback(()=>{
setCounter(prevState => ++prevState)
},[]);
const onClickSecond = React.useCallback(()=>{
setCounter2(prevState => ++prevState)
},[]);
const formatCounter = React.useCallback((counterVal)=> {
console.log('formatCounter Called')
return `The counter value is ${counterVal}`;
},[counter])
const formatCounter2 = React.useCallback((counterVal2)=> {
console.log('formatCounterTwo Called')
return `The counter value2 is ${counterVal2}`;
},[counter2])
const objMemo = React.useMemo(()=> {
console.log('obj memo is')
return {
a:counter>2?'counter is bigger than 2': 'counter is less than 2'
}
},[counter])
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<div>{formatCounter2(counter2)}</div>
<button onClick={onClick}>
Increment
</button>
<button onClick={onClickSecond}>
Increment2
</button>
<p>{objMemo.a}</p>
</div>
);
}
link to code

useCallback memoizes the function reference. But you're calling the format functions (both) in the render method. So what you're actually seeing is the inner function being called.
Maybe out of curiosity you can check the reference to the function you're memoizing. There you will see that the reference is not changing.
To check this I usually do:
useEffect(() => {
console.log('formatCounter reference changed!', formatCounter);
}, [formatCounter]);
You will see that it logs for when counter changes but not for when counter2 changes.

Related

Callback function created in custom react hook isn't being provided with an up to date value

In a custom alert system I have created in react I have 2 main functions, a custom hook:
function useAlerts(){
const [alerts, setAlerts] = useState([]);
const [count, setCount] = useState(0);
let add = function(content){
let remove = function(){
console.log(`alerts was set to ${alerts} before remove`);
setAlerts(alerts.slice(1));
};
let newAlert =
<div className = 'alert' onAnimationEnd = {remove} key = {count}>
<Warning/>
<span>
{content}
</span>
</div>
setAlerts([...alerts, newAlert]);
setCount(count + 1);
}
return [alerts,add];
}
and another element to display the data within the custom hook.
function Alerts(){
let [alerts,add] = useAlerts();
useEffect(() => {
let handler = function(){
add('test');
};
window.addEventListener('keyup',handler);
return function(){
window.removeEventListener('keyup',handler);
}
});
return (
<div className = 'alerts'>
{alerts}
</div>
)
}
the current issue I have is with the remove callback function, the console will look something like this.
let remove = function(){
console.log(`alerts was set to ${alerts} before remove`);
/// expected output: alerts was set to [<div>,<div>,<div>,<div>] before remove
/// given output: alerts was set to [] before remove
setAlerts(alerts.slice(1));
};
I understand this is because the remove function is taking the initial value of the alert state, but how do I make sure it keeps an up to date value? React doesn't seem to allow useEffect in a callback so I seem to be a bit stuck. Is passing the value in an object the way to go or something?
The issue I believe I see here is that of stale enclosures over the local React state. Use functional state updates to correctly update from the previous state instead of whatever if closed over in callback scope. In fact, use functional state updates any time the next state depends on the previous state's value. Incrementing counts and mutating arrays are two prime examples for needing functional state updates.
function useAlerts() {
const [alerts, setAlerts] = useState([]);
const [count, setCount] = useState(0);
const add = (content) => {
const remove = () => {
console.log(`alerts was set to ${alerts} before remove`);
setAlerts(alerts => alerts.slice(1));
};
const newAlert = (
<div className='alert' onAnimationEnd={remove} key={count}>
<Warning/>
<span>
{content}
</span>
</div>
);
setAlerts(alerts => [...alerts, newAlert]);
setCount(count => count + 1);
}
return [alerts, add];
}

Why the count value increment by 1 not by 2 with two setState in React

Here's the code that does not work :
import { useState } from "react";
const Counter = () => {
const [count,setCount]= useState(0);
const buttonHandler = ()=>{
setCount(count+1);
setCount(count+1);
}
return (
<div >
{count}
<button onClick={buttonHandler}>+</button>
</div>
);
}
I don't really get what is happening under the hood of React. I saw through some videos it would work if I did this:
const buttonHandler = ()=>{
setCount(prevCount => prevCount+1);
setCount(prevCount => prevCount+1);
}
But I don't feel like I really get why the first one is not working
In your function, buttonHandler, setCount isn't changing the value of count immediately. It's just updating the state that is used to set count the next time Counter is rendered.
So if count is 0, calling setCount with a value of count + 1 twice actually results in two calls to setCount with a value of 1.

State update faster than alert display

import React from 'react';
const Comp2 = () => {
const [count, setCount] = React.useState(0);
const handleIncrease = () => {
setCount((x) => x + 1);
};
const checkCurrentCount = () => {
console.log('checking...');
setTimeout(() => {
alert(`Ok the current count is: ${count}`);
}, 2000);
};
return (
<div>
<p>{count}</p>
<button onClick={handleIncrease}>+</button>
<button onClick={checkCurrentCount}>check count</button>
</div>
);
};
Problem
If the count number already change, but the alert shows the past number. How to encounter this problem? the setTimeout just simulation of the problem..
You can fix this issue with help of useRef hook.
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
View the solution on code sandbox
https://codesandbox.io/s/priceless-newton-l66q6?file=/src/App.js
You need clearTimeout and setTimeout again
CodeSandBox

useCallback with dependency vs using a ref to call the last version of the function

While doing a code review, I came across this custom hook:
import { useRef, useEffect, useCallback } from 'react'
export default function useLastVersion (func) {
const ref = useRef()
useEffect(() => {
ref.current = func
}, [func])
return useCallback((...args) => {
return ref.current(...args)
}, [])
}
This hook is used like this:
const f = useLastVersion(() => { // do stuff and depends on props })
Basically, compared to const f = useCallBack(() => { // do stuff }, [dep1, dep2]) this avoids to declare the list of dependencies and f never changes, even if one of the dependency changes.
I don't know what to think about this code. I don't understand what are the disadvantages of using useLastVersion compared to useCallback.
That question is actually already more or less answered in the documentation: https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
The interesting part is:
Also note that this pattern might cause problems in the concurrent mode. We plan to provide more ergonomic alternatives in the future, but the safest solution right now is to always invalidate the callback if some value it depends on changes.
Also interesting read: https://github.com/facebook/react/issues/14099 and https://github.com/reactjs/rfcs/issues/83
The current recommendation is to use a provider to avoid to pass callbacks in props if we're worried that could engender too many rerenders.
My point of view as stated in the comments, that this hook is redundant in terms of "how many renders you get", when there are too frequent dependencies changes (in useEffect/useCallback dep arrays), using a normal function is the best option (no overhead).
This hook hiding the render of the component using it, but the render comes from the useEffect in its parent.
If we summarize the render count we get:
Ref + useCallback (the hook): Render in Component (due to value) + Render in hook (useEffect), total of 2.
useCallback only: Render in Component (due to value) + render in Counter (change in function reference duo to value change), total of 2.
normal function: Render in Component + render in Counter : new function every render, total of 2.
But you get additional overhead for shallow comparison in useEffect or useCallback.
Practical example:
function App() {
const [value, setValue] = useState("");
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
type="text"
/>
<Component value={value} />
</div>
);
}
function useLastVersion(func) {
const ref = useRef();
useEffect(() => {
ref.current = func;
console.log("useEffect called in ref+callback");
}, [func]);
return useCallback((...args) => {
return ref.current(...args);
}, []);
}
function Component({ value }) {
const f1 = useLastVersion(() => {
alert(value.length);
});
const f2 = useCallback(() => {
alert(value.length);
}, [value]);
const f3 = () => {
alert(value.length);
};
return (
<div>
Ref and useCallback:{" "}
<MemoCounter callBack={f1} msg="ref and useCallback" />
Callback only: <MemoCounter callBack={f2} msg="callback only" />
Normal: <MemoCounter callBack={f3} msg="normal" />
</div>
);
}
function Counter({ callBack, msg }) {
console.log(msg);
return <button onClick={callBack}>Click Me</button>;
}
const MemoCounter = React.memo(Counter);
As a side note, if the purpose is only finding the length of input with minimum renders, reading inputRef.current.value would be the solution.

setInterval and React hooks produces unexpected results

I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!
Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX
That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!
Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}

Resources