Why React render 3 times rather than 2 after setInterval? - javascript

As the function body of React function component is like the render function of the React class component, the React function component should be executed repeatedly whenever it's rendered.
So I wrote below code to verify this behavior.
import { useEffect, useState } from "react";
import "./styles.css";
let i = 0;
export default function App() {
console.log("App:", ++i);
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => setCount(count + 1), 2000);
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Count:{count}</h2>
</div>
);
}
I expect the console output as below:
App: 1
App: 2
But the actual result is as below:
App: 1
App: 3
App: 5
Please check:Example code in codesandbox
My understanding is that, i should be 1 when App component is executed for the first time.
After 2 seconds, setCount(0+1) update count from 0 to 1, React triggers a re-rendering which run App function for the second time, and should print "App: 2" in console. Every 2 seconds after that, setCount(0+1) will be executed repeatedly without triggering any count update as count is always 1 after that.
But the actual result is totally different from my expectation.
Very appreciate if anyone could help correcting my misunderstanding.

How many times your component function is called doesn't matter and shouldn't matter for your app.
React is absolutely free to call your component function a thousand times per update if it feels like it (and indeed, in strict mode it does so twice); that's why you're supposed to make them idempotent and to use React.useMemo if you have heavy computations.
However, your component is broken in that the effect doesn't capture count, so the count may not correctly increase. (The rules of hooks ESlint rules will let you know.) You'd want the function form of setState to derive the new state from the old one:
setCount(count => count + 1);

Related

React JS: Why is my cleanup function triggering on render? [duplicate]

I have a counter and a console.log() in an useEffect to log every change in my state, but the useEffect is getting called two times on mount. I am using React 18. Here is a CodeSandbox of my project and the code below:
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
useEffect(() => {
console.log("rendered", count);
}, [count]);
return (
<div>
<h1> Counter </h1>
<div> {count} </div>
<button onClick={() => setCount(count + 1)}> click to increase </button>
</div>
);
};
export default Counter;
useEffect being called twice on mount is normal since React 18 when you are in development with StrictMode. Here is an overview of what they say in the documentation:
In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React will support remounting trees using the same component state used before unmounting.
This feature will give React better performance out-of-the-box, but requires components to be resilient to effects being mounted and destroyed multiple times. Most effects will work without any changes, but some effects do not properly clean up subscriptions in the destroy callback, or implicitly assume they are only mounted or destroyed once.
To help surface these issues, React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.
This only applies to development mode, production behavior is unchanged.
It seems weird, but in the end, it's so we write better React code, bug-free, aligned with current guidelines, and compatible with future versions, by caching HTTP requests, and using the cleanup function whenever having two calls is an issue. Here is an example:
/* Having a setInterval inside an useEffect: */
import { useEffect, useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000);
/*
Make sure I clear the interval when the component is unmounted,
otherwise, I get weird behavior with StrictMode,
helps prevent memory leak issues.
*/
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
export default Counter;
In this very detailed article called Synchronizing with Effects, React team explains useEffect as never before and says about an example:
This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development.
For your specific use case, you can leave it as it's without any concern. And you shouldn't try to use those technics with useRef and if statements in useEffect to make it fire once, or remove StrictMode, because as you can read on the documentation:
React intentionally remounts your components in development to help you find bugs. The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”.
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).
/* As a second example, an API call inside an useEffect with fetch: */
useEffect(() => {
const abortController = new AbortController();
const fetchUser = async () => {
try {
const res = await fetch("/api/user/", {
signal: abortController.signal,
});
const data = await res.json();
} catch (error) {
if (error.name !== "AbortError") {
/* Logic for non-aborted error handling goes here. */
}
}
};
fetchUser();
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application.
In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned... So even though there is an extra request, it won’t affect the state thanks to the abort.
In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:
function TodoList() {
const todos = useSomeDataFetchingLibraryWithCache(`/api/user/${userId}/todos`);
// ...
Update: Looking back at this post, slightly wiser, please do not do this.
Use a ref or make a custom hook without one.
import type { DependencyList, EffectCallback } from 'react';
import { useEffect } from 'react';
const useClassicEffect = import.meta.env.PROD
? useEffect
: (effect: EffectCallback, deps?: DependencyList) => {
useEffect(() => {
let subscribed = true;
let unsub: void | (() => void);
queueMicrotask(() => {
if (subscribed) {
unsub = effect();
}
});
return () => {
subscribed = false;
unsub?.();
};
}, deps);
};
export default useClassicEffect;

how to calculate default value for react js state hook with a function?

I'm doing something like this:
function App() {
const [state1, setState1] = useState([1,1,1,1,1,1]);
const [state2, setState2] = useState(MyFunction());
return (
<div className="App">
<button onClick={() => setState1([1,2,3,4,5,6])}>test</button>
</div>
);
}
const MyFunction= () => {
alert("MyFunction");
return 5;
}
The strange thing is that the line alert("MyFunction"); is triggered 2 times on load and 2 times on every click on test button.
I'm very new to React.js and I don't understand this behavior.
To answer your question:
how to calculate default value for react js state hook with a function?
useState allows a function as a 'initial state factory', like e.g.:
const [ state, setState ] = useState( function(){ return Math.random(); } );
So if you want to use your MyFunction as a factory, just use it this way:
const [ state2, setState2 ] = useState( MyFunction );
why is MyFunction called on every click ?
A React functional component is just a javascript function.
React decides when to call this function, which is basically whenever something changes. A call of some setState() is one reason why
React will call the function of the functional component again (your App() function in this case).
But I suggest you consider the App() function to be called "whenever React wants to call it", or "all the time, again and again". Meaning you should not
rely on when the function of the functional component is called, you should instead rely on the guarantees which React makes
regarding when the state is up-to-date, specifically useEffect, useState, ...
MyFunction() is just a function call, which is inside the App() function call, so - of course - MyFunction() is called
whenever App() is called (which is "again and again").
Why is the alert() called twice ?
The functional component is called 2 times in Strict Mode.
This causes unexpected behavior only if you aren't using React as it is supposed to be used (which is something that just happens for React-beginners).
If you are using React in the intended way, you should not have to care about if the function is called once, twice or multiple times. The only thing that counts is what the state is.
See also e.g. React.Component class constructor runs once without console.log?

How many time React should render?

I wrote a simple React to test how React render
import ReactDOM from 'react-dom';
import React, { useState, useEffect } from 'react';
const App = (props) => {
// State
const [trackIndex, setTrackIndex] = useState(1);
const [trackProgress, setTrackProgress] = useState(0);
const clickHandler = () =>{
setTrackIndex((trackIndex)=>trackIndex+1)
}
useEffect(() => {
console.log("effect; trackIndex=",trackIndex)
console.log("effect; trackProgress=",trackProgress)
if(trackIndex<5){
console.log("set trackProgress")
setTrackProgress(trackIndex)
}
});
console.log("render")
return (
<div>
<p>{trackIndex}</p>
<p>{trackProgress}</p>
<button onClick={clickHandler}>Click me</button>
</div>
);
};
ReactDOM.render(
<App />,
document.getElementById('root')
);
The following is the console output:
render
effect; trackIndex= 1
effect; trackProgress= 0
set trackProgress
render
effect; trackIndex= 1
effect; trackProgress= 1
set trackProgress
render
It seems that React renders three times before I click the button. The last rendering really confuses me. Could anyone explain to me why this render occurs and why no effect runs after this rendering? Thank you in advance
It seems that your useEffect is setting the state with the setTrackProgress on the initial render of the component. This is because trackIndex starts at 1 which is lower than 5. (seen by the if(trackIndex<5)) Notice that you haven't provided array dependency to useEffect as the second parameter. According to react-documentation this means the effect will only occur once after the first initial render of the component.
Also, I would suggest adding useCallback to your clickHandler in order to prevent re-defining the function for every render. (According to react-documentation)
You're practically making an infinite loop, but React saves you. The first render log is the initial render, then the useEffect is executed and your component re-renders, leading to the 2nd render log and the first effect; ... log. Now a 2nd useEffect is executed. However, the value of trackIndex hasn't changed, so your if statement will evaluate true and updates trackProgress with the same state/value(1). This leads to another re-render and the third render log. Now, you would think that a 3rd useEffect would be executed, but React is smart enough to know when state hasn't changed and thus doesn't execute the useEffect.
Add dependencies to your useEffect as stated above. That'll solve your problem.
useEffect(
...
, [trackIndex])

Weird React render issues [duplicate]

This question already has answers here:
React useState cause double rendering
(3 answers)
What is StrictMode in React?
(4 answers)
Closed 1 year ago.
So after working with react in the past year, i managed to understand its powers and caveats, and how to avoid unnecessary renders.
Yesterday i was playing with some code and came across an issue i didnt see before, and got a little confuse.
import React, { useCallback, useState } from "react";
let renders = 0;
const Counter = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);
renders = renders + 1;
console.log("Counter Render");
return (
<div>
<button onClick={increment}>increment</button>
<h2>renders: {renders}</h2>
<h4>Count: {count}</h4>
</div>
);
};
export default React.memo(Counter);
in the above code i added a simple counter, on every click i am setting a new state, which cause a re-render, showing "Count: 1" on the screen and one "Counter Render" log in dev tools - just like i expected.
The weird part comes from the renders variable, which i initiate with the number 0 ( outside the component, offcourse ) and increment on every render of the component.
i would have expect the value here will also be 1 but that is not the case, every time i click the button the value of "renders" grows by 2, even though the "Counter Render" log show once each time.
Here is a the Sandbox example
can anyone explain what am i missing here ?
You're running in React's Strict Mode (since your app is wrapped in <StrictMode>).
Strict Mode may render components multiple times to make sure you're not doing naughty things such as relying on render counts – which you now are. ;-)

Why does calling useState's setter with the same value subsequently trigger a component update even if the old state equals the new state?

This problem occurs only if the state value was actually changed due to the previous update.
In the following example, when the button is clicked for the first time, "setState" is called with a new value (of 12), and a component update occurs, which is understandable.
When I click the same button for the second time, setting the state to the same value of 12 it causes the component to re-run (re-render), and why exactly that happens is my main question.
Any subsequent setStates to the same value of 12 will not trigger a component update, which is again, understandable. 12 === 12 so no update is needed.
So, why is the update happening on the second click of the button?
export default function App() {
const [state, setState] = useState(0);
console.log("Component updated");
return (
<div className="App">
<h1>Hello CodeSandbox {state}</h1>
<button onClick={() => setState(12)}>Button</button>
</div>
);
}
Codesandbox example
The main question is, why logging in function component body causes 3 logs of "Component updated"?
The answer is hiding somewhere in React docs:
if you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects.
Nothing new, but then:
Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree.
But notice useEffect API definition:
will run after the render is committed to the screen.
If you log the change in useEffect you notice only two "B" logs as expected, which is exactly the example for bail out behavior mentioned:
const App = () => {
const [state, setState] = React.useState(0);
useEffect(() => {
console.log("B");
});
console.log("A");
return (
<>
<h1>{state}</h1>
<button onClick={() => setState(42)}>Click</button>
</>
);
};
There will be an additional "Bail out" call for App component (extra "A" log), but React won't go "deeper" and won't change the existing JSX or state (no additional "B" will be logged).
Adding to the generally correct accepted answer, there is what i've found diving deeper in that problem:
There is actually more complex mechanics in that.
Actually, any setState call is applying reducer function under the hood, and that reducer function runs on next useState call, not before component function execution.
So normally there is no way of knowing if new state will be the same or not without executing executing that reducer (on useState call).
On the other hand however, when such reducer was once executed and state was not changed, component's return is ignored (render skipped) and next call of that reducer will be executed before component's function and NOT when useState called. That is also true for the very first setState of the component's life.
I've made a demo of that in codesandbox

Categories