Different function gets passed to useEffect on every render? - ReactJS useEffect - javascript

I was going through useEffect from reactjs docs and I've found this statement here
Experienced JavaScript developers might notice that the function
passed to useEffect is going to be different on every render.
We are passing a function to useEffect and this function is said to be different for each render. useEffect has access to state and props since it's inside the function component and when either of these changes, we can see that change in the function of useEffect(because of closure) right? This is not clear, because in the next line the doc states
This is intentional. In fact, this is what lets us read the count
value from inside the effect without worrying about it getting stale.
To counter this, assume we have a function
function foo(n) {
bar = () => {
setTimeout(() => console.log({n}), 50);
return n;
}
setTimeout(() => {n = 10}, 0);
setTimeout(() => {n = 20}, 100);
setTimeout(() => {n = 30}, 150);
return bar;
}
baz = foo(1);
baz(); //prints 10
setTimeout(baz, 300); //prints 30
It seems that when the closure value(n) is changed, we can see that change in the setTimeout's callback (and this callback isn't changed over time). So, how can the closured value(state/props) in useEffect's function become stale as mentioned in docs?
Am I missing something here? I think it's more of a JS question compared to React, so I took a JS example.

I found the answer a few days back, and as #apokryfos(Thank you again!) mentioned in the comments above, the program execution process is now making more sense. I want to summarize my learnings here.
Firstly, the code I considered, was not like with like comparison (in #apokryfos words) with the React doc statements, and this is true. In case of static HTML + vanilla JS where the HTML button has an event-listener JS function, this function is declared only once and when the event occurs the same JS function is executed everytime.
The code I have given in the question is similar to this, and so when executed in console or in event listener will only be declared once.
In case of React(or any state based UI libraries/frameworks), the HTML is not static and it needs to change on state-change. On the execution side (considering React), component will be created when we call it (in JSX), and the component's function/class will be executed completely from top to bottom. This means
from all the event-handlers that doesn't deal with state, constants to useState's destructed elements and useEffect's callback functions, everything are re-initialized.
If the parent's state changes initiate a render on its children(in normal scenarios), then the children will need to re-render themselves completely with the new props and/or new state to show the updated UI
Considering the example in React docs (link), useEffect with no dependencies will get executed after every render, and here it's updating the DOM by showing the current state value. Unless the callback function has the latest value of that state, it'll only print the stale value. So re-initialising the functions here is the main reason behind not having stale values in the callback functions
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = 'You clicked ${count} times';
});
}
This is a boon and a curse sometimes. Assume if we are sending useState's setState function to the child, and in the child, we have a useEffect(that makes a network call to fetch data) that takes this function as its dependency. Now, for every state change in parent, even if there is a use-case or not, the useEffect will get triggered as this function in dependency is changed (as every update re-initializes the functions). To avoid this, we can utilize useCallback on the functions which we want to memorize and change only when certain params are changed, but it is not advisable to use this on useEffect's callback function since we might end-up in stale values.
Further Reading:
GeeksForGeeks useCallback
SourceCode interpretation of useEffect
Another SourceCode interpretation of useEffect

Related

Not updating the State inside the function but updating in useEffect

I'm a beginner to React JS.
I faced this weird situation,
const [counter, setCounter] = useState(0);
const incrementCounter = () => {
setCounter(counter + 1);
console.log(counter, "Log inside the function");
};
useEffect(() => {
console.log(counter, "Log inside the useEffect");
}, [counter]);
return (
<div>
<h1>{counter}</h1>
<button onClick={incrementCounter}>Increment</button>
</div>
);
So when incrementCounter function is triggered counter value will be increased by 1 and right after that I'm logging the value in the console. But it displays the value as 0. When I use the useEffect hook to check the changes of count state it I'm getting the correct value as 1 when I log the count value inside the useEffect scope. What is the reason for not displaying in incrementCounter function and the reason for displaying the correct value inside the useEffect hook.
according to React docs
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. In the rare case that you need to force the DOM update to be applied synchronously, you may wrap it in flushSync, but this may hurt performance.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
States value will change only after the component has rendered.
In this case the log inside the function will keep the state value even after the SetCounter, once it completes the component will render again.
The useEffect is triggered when the state has changed on such render so it shows the new value.
A new incrementCounter function object is being redefined with every render. Each time it is defined, its own internal counter is based on the value of counter state at that point. The counter within that particular incrementCounter function object is stale and doesn't get updated anymore.
This is because the setCounter is asynchronous.
If you use setTimeOut you can clearly observe it
setTimeout(() => {
console.log(counter, "Log inside the function");
}, 2000);

React - in a functional component, using hooks, can I execute a piece of code once after one setState() successfully changes state?

setState updates state asynchronously. It's my understanding that, when using a class component, you can do something like this to ensure certain code is executed after one setState updates state:
setState({color: red}, callbackThatExecutesAfterStateIsChanged);
I'm using a functional component & hooks. I'm aware, here, useEffect()'s callback will execute everytime after color state changes and on initial execution.
useEffect(callback, [color]);
How can I replicate similar behaviour as the class component example - that is, to execute a chunk of code once after one setState() successfully changes state and not on initial execution?
If you ask me, there is no safe way to do this with hooks.
The problem is that you both have to read and set an initialized state in order to ignore the first update:
const takeFirstUpdate = (callback, deps) => {
const [initialized, setInitialized] = useState(false);
const [wasTriggered, setWasTriggered] = useState(false);
useEffect(() => {
if (!initialized) {
setInitialized(true);
return;
}
if (wasTriggered) {
return;
}
callback();
setWasTriggered(true);
}, [initialized, wasTriggered]);
};
While the hook looks like it works, it will trigger itself again by calling setInitialized(true) in the beginning, thus also triggering the callback.
You could remove the initialized value from the deps array and the hook would work for now - however this would cause an exhaustive-deps linting error. The hook might break in the future as it is not an "official" usage of the hooks api, e.g. with updates on the concurrent rendering feature that the React team is working on.
The solution below feels hacky. If there's no better alternative, I'm tempted to refactor my component into a class component to make use of the easy way class components allow you to execute code once state has been updated.
Anyway, my current solution is:
The useRef(arg) hook returns an object who's .current property is set to the value of arg. This object persists throughout the React lifecycle. (Docs). With this, we can record how many times the useEffect's callback has executed and use this info to stop code inside the callback from executing on initial execution and for a second time. For example:
initialExecution = useRef(true);
[color, setColor] = useState("red");
useEffect(() => {
setColor("blue");
});
useEffect(() => {
if (initialExecution.current) {
initialExecution.current = false;
return;
}
//code that executes when color is updated.
}, [color]);

React call back on set state

index.js:1 Warning: State updates from the useState() and useReducer()
Hooks don't support the second callback argument. To execute a side
effect after rendering, declare it in the component body with
useEffect().
This is the error I get when trying to do this
setUnansweredQuestions({ newUnansweredQuestions }, () =>
nextQuestion()
);
I tried run a function after updating the state for unanswered questions but won't work cause it doesn't update right away.
I searched a bit and it is said to use useEffect but I already have one defined and won't let me create another. I just want to call the function nextQuestion after updating UnansweredQuestions
useEffect(() => {
setUnansweredQuestions(questions);
selectRandomQuestion();
}, [currentQuestion]);
There's nothing wrong with having multiple useEffects.
Since you do setUnansweredQuestions and want to run something after that state variable changes, just do:
useEffect(nextQuestion, unansweredQuestions);
You can try like this, I know this is not a good solution, but it works
setState(UnansweredQuestions);
setTimeOut(() => {
nextQuestion()
}, 16)

Why does the load() function with [] as the second argument not run on mount unless I pass in [load] to the second argument?

Why does the load() function with [] as the second argument not run on mount unless I pass in [load] to the second argument? I do not want to pass in the second argument as I only want the load() function to run once on mount and never afterward. How do I achieve this?
const HoursTimer = (props) => {
const [isActive, setIsActive] = useState(null);
const [activeId, setActiveId] = useState(null);
useEffect(() => {
load();
}, [])
function load() {
props.hours.forEach((hour, i) => {
if (i === (props.hours.length-1)) {
setIsActive(hour.clockedIn)
setActiveId(hour._id)
}
})
}
return (
<div>
<p>{isActive}</p>
<p>{activeId}</p>
</div>
);
};
function mapStateToProps( { hours } ) {
return { hours };
}
export default connect(mapStateToProps, { fetchHours })(HoursTimer);
I've never used React before, but a quick review of the useEffect source, and I think I have and answer for you.
useEffect takes two arguments, as you know. But did you know the array, aka "deps" tells React, when you want to rerun the effect? React does this by "rechecking" those deps.
So a short hand for run once, but never again is an empty array.
I confirmed this from React's documentation
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
If you pass an empty array ([]), the props and state inside the effect
will always have their initial values. While passing [] as the second
argument is closer to the familiar componentDidMount and
componentWillUnmount mental model, there are usually better solutions
to avoid re-running effects too often. Also, don’t forget that React
defers running useEffect until after the browser has painted, so doing
extra work is less of a problem.
Another insightful comment from the documentation
The array of dependencies is not passed as arguments to the effect
function. Conceptually, though, that’s what they represent: every
value referenced inside the effect function should also appear in the
dependencies array. In the future, a sufficiently advanced compiler
could create this array automatically.
So depending on you desired behavior, if you want to run the effect, everytime the prop.hours updated you might need to do something like
useEffect(() => {
load();
}, [props.hours])
function load() {
props.hours.forEach((hour, i) => {
if (i === (props.hours.length-1)) {
setIsActive(hour.clockedIn)
setActiveId(hour._id)
}
})
}
Looking into things a bit more, I found this article, which may be helpful.
As for you question:
Why does the load() function with [] as the second argument not run on
mount unless I pass in [load]
I suspect, it's because [load] is a function, which react, will rerun, but
it's doing so to check if it needs to rerun the effect... with the side effect, of well, having already, rerun load, which just so happens to be your entire useEffect. -- that was at least 3 levels of inception.
Hope that helps.

React Performance Issues in Firefox?

I'm experiencing some performance issues with a react application that I developed. These issues specifically (or most notably) occur with Firefox (both FF developer 77.0b7 and FF 76.0.1).
When using this application in Firefox, CPU usage gets extremely high, and my fans start spinning up to very high speeds. I get about 15-19fps in firefox according to the performance tools in FF. I get roughly 60fps in Chrome and Safari.
These issues occur when I begin typing into the input field, and get worse as the input gets longer (which makes sense)
The application is available here:
https://text-to-aura-generator.netlify.app/
Source code available here: https://github.com/paalwilliams/Text-to-Aura/tree/master/src
I'm almost certain that this is something I'm doing incorrectly, or that I've written the code inefficiently, but that isn't necessarily supported by the stark performance difference between browsers. Is chrome just that much better and handling react/constant rerenders?
I know that this is a broad question, but I honestly don't understand what is happening here, or necessarily how to troubleshoot it beyond the developer tools. Any input or thoughts would be greatly appreciated.
The problem is your application is rendering too fast. In your particular case, there a few ways to improve that.
Every time you update the state, React needs to re-render your application, so updating the state within a loop is usually a bad idea.
Also, you are using useState 3 times, but only colors should be there, as App actually needs to re-render to reflect the changes there. The other two pieces of state (text and hex) are only being used to pass data from the handleChange to the callback inside useEffect.
You can restructure your code to:
Avoid updating the state within a loop.
Use a simple variable instead of state.
Use useCallback to define a function with that logic that is not re-created on each render, as that forces TextInput to re-render as well.
Throttle this callback using something like this:
import { useCallback, useEffect, useRef } from 'react';
export function useThrottledCallback<A extends any[]>(
callback: (...args: A) => void,
delay: number,
deps?: readonly any[],
): (...args: A) => void {
const timeoutRef = useRef<number>();
const callbackRef = useRef(callback);
const lastCalledRef = useRef(0);
// Remember the latest callback:
//
// Without this, if you change the callback, when setTimeout kicks in, it
// will still call your old callback.
//
// If you add `callback` to useCallback's deps, it will also update, but it
// might be called twice if the timeout had already been set.
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Clear timeout if the components is unmounted or the delay changes:
useEffect(() => window.clearTimeout(timeoutRef.current), [delay]);
return useCallback((...args: A) => {
// Clear previous timer:
window.clearTimeout(timeoutRef.current);
function invoke() {
callbackRef.current(...args);
lastCalledRef.current = Date.now();
}
// Calculate elapsed time:
const elapsed = Date.now() - lastCalledRef.current;
if (elapsed >= delay) {
// If already waited enough, call callback:
invoke();
} else {
// Otherwise, we need to wait a bit more:
timeoutRef.current = window.setTimeout(invoke, delay - elapsed);
}
}, deps);
}
If the reason to use useEffect is that you were not seeing the right values when updating colors, try using the version of setState that takes a callback rather then the new value, so instead of:
setColors([...colors, newColor]);
You would have:
setColors(prevColors => ([...prevColors , newColor]));
The most common performance issues with react come from setting the state too many times since you're constantly re rendering the page and the elements within it.

Categories