Re-rendering in React functional component - why this behavior? - javascript

This question concerns the definition of callback functions inside functional components, with the callback functions being passed down as props to child functional components.
I am aware that when defining functions inside functional components and those functions are being passed on as props to child components we should use useCallback. In this example I am deliberately not using it.
I have a dummy React app as below:
App.jsx
import { useState, useCallback } from 'react';
import './App.css';
import TestComponent from './components/TestComponent';
import TestComponent2 from './components/TestComponent2';
function App() {
const [message, setMessage] = useState("");
console.log('Defining myArrowfunc');
const myArrowFunc = () => console.log('Hello!');
console.log('myArrowfunc defined');
return (
<>
<TestComponent message={message} myArrowFunc={myArrowFunc} />
<TestComponent2 myArrowFunc={myArrowFunc} />
<input
type="text"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button
type="button"
onClick={() => {
console.log('Setting message');
setMessage("x");
}}>Set to x</button>
</>
);
}
export default App;
TestComponent.jsx
import { useEffect } from 'react';
function TestComponent(props) {
useEffect(() => {
console.log(`TestComponent.useEffect, props.message = ${props.message}`);
}, [props]);
return <h2>TestComponent - {props.message}</h2>;
}
export default TestComponent;
TestComponent2.jsx
import { useEffect } from 'react';
function TestComponent2(props) {
useEffect(() => {
console.log(`TestComponent2.useEffect`);
}, [props.myArrowFunc]);
return <h2>TestComponent2 - Placeholder</h2>;
}
export default TestComponent2;
Starting the app, the page loads and we are here:
In the console window everything looks like expected. The console.log statements Defining myArrowFunc and myArrowFunc defined ran, and we can see the console.log statements from the useEffect hook within TestComponent and TestComponent2 which ran after they were rendered.
Now we click on the button Set to x, which invokes the callback function attached to the onClick event of the button. This callback calls setMessage("x") which updates the state of the App component and consequently triggers a re-render of the component.
In the console window we can see that the App functional component ran (e.g. from the console.log("Defining myArrowFunc)) and one can also see that the useEffect hooks of the child components ran after they were re-rendered.
Now, that TestComponent re-rendered is of course understandable. message state is a prop on TestComponent which would cause a re-render of TestComponent. Also, in the dependency array of TestComponent is specified props (not props.message), but props is a new object on every render (right?) and hence the reference comparison tells us props has changed and so useEffect runs.
That the useEffect hook of TestComponent2 ran can be understood (right?) from the fact that myArrowFunc is re-defined on each render of App, and so the prop passed to TestComponent2 has in fact changed (reference comparison).
Here comes the part where I become confused: the message state in App now holds "x", and so additional clicks to the button will not change the state, and so should not trigger a re-render of App (and any child components dependant). When I click the button the following output happens:
The statements console.log('Defining myArrowfunc'); and console.log('myArrowfunc defined'); ran. What does this mean? Did the component re-render? If the App function ran, then it should have defined a new myArrowFunc (right?), and since TestComponent2 takes that as a prop, it should have re-rendered and run its useEffect hook?
What's interesting as well is that when I again click the button, it does not look like App ran, the output becomes only "Setting message".
Would very much appreciate an outline / explanation of what exactly is going on here.

When you click the button, a console log with setting message will run, and then a setState with will make a re-render.
At the following click, react is clever enough to see that the state you are setting is the same as before so it doesn't re-render
If you change then, the value and click the button, it will re-render, but not for the following cases with the same value.
To sum up, the first's console.log won't run if there's not a re-render, and the procedure of react--if default--memoizing will prevent it

Related

Issue with using useLayoutEffect React

Hook useEffect runs asynchronously and usually after the DOM is rendered/mounted, whereas useLayoutEffect runs synchronously and before the DOM is rendered/mounted.
Using my example useEffect, all works fine and I get desired result (I store the previous value that the user has entered using useRef).
But using my example useLayoutEffect, I don't get the wanted result, cause it just works as useEffect in my case. I reckon that it should firstly update prevName.current = name and only after render causing the same value that person has entered. I think, that in that case both prevName.current and name should be rendered as same values. But that doesn't happen.
Can you please explain why?
Here's codesandbox: Sandbox
Here's the code:
import React, { useLayoutEffect } from "react";
import "./styles.css";
import { useState, useRef, useEffect } from "react";
export default function App() {
const [name, setName] = useState("");
const prevName = useRef(name);
useLayoutEffect(() => {
prevName.current = name;
}, [name]);
return (
<div className="App">
<input
value={name}
onChange={(e) => {
setName(e.target.value);
}}
></input>
<div>Your name is {name}</div>
<div>Your previous name is {prevName.current}</div>
</div>
);
}
Hook useEffect runs asynchronously and usually after the DOM is rendered / mounted, whereas useLayoutEffect runs synchronously and before the DOM is rendered / mounted.
Both of them occur after the changes have been put onto the DOM. What's special about layout effects is that if you set state during the layout effect, the resulting render will be done synchronously. For normal effects, if you set state, it will rerender soon, but not synchronously. If you never set state (as in your example), it's not going to make much difference which one you use.
The most common application of useLayoutEffect is that you want to render once, measure the stuff on the dom, and then render again without the user seeing the temporary stuff you had on the dom.

Number of console logs in component body not equal to number of rerenders?

I have a simple React component that is just supposed to console.log some dummy text when component is rendered. I have read some articles about rendering and I would assume that rendering happens at the same time as invoking the component, i.e. rendering is not possible without invoking/executing component function a priori.
So, the number of console logs in this case should be 2 for each - once when component initially renders, and second time when I click on the button and i is set to 2. Every next time React should be able to detect that state is actually not the same and not re-render the component.
But, I get "Hello from invoking" (that is not in use effect but in component body) three times, once when component initially renders, and two times when I click on the button. This component is for sure rendered just twice, so where did that third console.log come from? Is it possible that the component function is invoked, but not re-rendered? In contrary, "Hello from rendering" is logged twice as I would assume.
import { useEffect, useState } from "react";
const DummyComponent = (props) => {
const [i, setI] = useState(0);
useEffect(() => {
console.log("hello from rendering!");
});
console.log("hello from invoking!");
return (
<div onClick={() => setI(2)}}> Hello </div>
);
};
export default DummyComponent;
When the onClick calls setI, this causes a change to the state and an update gets triggered. This will call the lifecycle methods associated with an update.
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
The useEffect hook is called during componentDidUpdate() (as well as componentDidMount), while the console.log outside the useEffect is going to be called during render().
That means we should expect to see 1 hello from rendering! and 1 hello from invoking! when we refresh or when we click the "Hello" div.
In this codesandbox example you can see that we are getting the expected behavior of one a single console.log of each.
Now in this codesandbox example we are using StrictMode, and you can see that when we click the "Hello", we are seeing hello from rendering! 1 time, but we are seeing hello from invoking! 2 times!
StrictMode was enabled by adding a <StrictMode> tag to index.js
root.render(
<StrictMode>
<App />
</StrictMode>
);
It seems that the behavior you are describing is coming from the use of StrictMode.
This happens because StrictMode intentionally double-invokes the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
Thus, we see the render function occurring 2 times, and we get the 2 console.logs.

React Hooks: Why I have this output?

I'm studying React hooks but i am not able to understand why I got this output in the console. Someone with great heart could explain in detail the "running execution" oh these hooks?
import { useState, useEffect } from "react";
function Homepage() {
const [state, setState] = useState();
useEffect(() => {
console.log("useEffect");
console.log("useEffect not executed");
setState("hello")
}, []);
if (!state) {
console.log("state not defined");
return <div>State undefined</div>;
}
return <div>ciao</div>;
}
export default Homepage;
console output:
state not defined
state not defined
useEffect
useEffect not executed
useEffect
useEffect not executed
Basically it's a combination of React.StrictMode double invoking the function body twice as a way to help you detect unexpected side-effects
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies <-- this
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
remounting the component to ensure reusable state
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.
and the useEffect hook being called at the end of the render cycle.
function Homepage() {
const [state, setState] = useState();
useEffect(() => {
console.log("useEffect"); // logs second as expected side-effect
console.log("useEffect not executed");
setState("hello");
}, []);
if (!state) {
console.log("state not defined"); // logs first as unintentional side-effect
return <div>State undefined</div>;
}
return <div>ciao</div>;
}
...
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import Homepage from "./Homepage";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<Homepage />
</StrictMode>
);
Explaining the logs
console output:
state not defined // <-- initial render
state not defined // <-- double invocation of function body
useEffect // <-- effect at end of initial render
useEffect not executed // <-- effect at end of initial render
...unmount/mount
useEffect // <-- effect at end of render
useEffect not executed // <-- effect at end of render
useEffect runs when the page renders and the other functions execute before useEffect so your code runs the if (!state) first then useEffect runs and state sets to "hello"; here is a link to fully understand useEffect hook: useEffect; Good luck;
your if statement is running before the useEffect even tho is after it, you need to put both divs inside the return and remove the if
return (
{!state ? <div>State undefined</div> : <div>ciao</div>}
)
state not defined first render of your component Homepage
state not defined 2nd render
useEffect useEffect not executed your component is mounted, so the useEffect hook is triggered.
useEffect useEffect not executed here your component rerender with the state hello the useEffect hook is triggered again
In practice, useEffect hook runs at minimum following your dependency array. Here your set [] so the useEffect hook runs when the component mounts, but sometimes it can still be triggered.

Why setState causes too many rerender Error even though the state hasn't changed

Hi I'm learning React now and having trouble with the state..
I know when the state changes, the component re-renders and the code in UseEffect widout depth only runs once.
But I can't explain exactly why infinite rendering occurs when I write setState in JSX or in the render syntax.
below code causes infinite re-render
import React, { useState, useEffect } from 'react'
const index = () => {
const [active, setActive] = useState(false);
console.log("render", active);
setActive(false);
return (
<div>
</div>
)
}
export default index
But the code below has no problem even though it keeps calling setState.
import React, { useState, useEffect } from 'react'
const index = () => {
const [active, setActive] = useState(false);
console.log("render", active);
useEffect(() => {
setInterval(()=>{
console.log("run")
setActive(true)
},0);
}, [])
return (
<div>
</div>
)
}
Does setState trigger re-rendering regardless of state value?
I want to know the exact reason why using setState outside of useEffect causes an error.
This is happening because, in the first case, when useEffect is not used,
you are updating your state right after declaring it.
Even if you are setting the state as false again, but for react, the state has been updated. And the first rule of thumb for react is, if state update happens, component will re render.
This is why you are getting an infinite rerendering.
your code is following the below flow:
Declare state variable and pass value as false
Update state to false
State updated, hence component re rendered.
Step 1 again.
In second case, where use effect is used, your state will update only when the component is mounted, which means, after that any state update won't trigger your useEffect.
Based on React documentation: https://reactjs.org/docs/hooks-reference.html#usestate
The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component.
And there is an addition: https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update
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. (React uses the Object.is comparison algorithm.) Note that React may still need to render that specific component again before bailing out.
And here is the important part: React may still need to render that specific component again before bailing out.
So yes, the component may still rerender even if the values are the same.

Why can I not use an inline function for a Screen component?

In the getting started of the React-Navigation documentation they say:
Note: The component prop accepts component, not a render function. Don't pass an inline function (e.g. component={() => }), or your component will unmount and remount losing all state when the parent component re-renders. See Passing additional props for alternatives.
From my understanding if I write:
const WrappedHome = () => <HomeScreen />;
Then later use this:
<Stack.Screen name='home' component={WrappedHome} />
That would be equivilent to their example. Is React or JSX or React-native doing something special to cause inline function in props to be evaluated differently than standard Javascript? Or what am i misunderstanding?
A component is not just a regular function. When you render a component, it has a "type" - which is the function that creates the component:
e.g:
function MyComponent() {
// whatever
}
<MyComponent /> // the component type is MyComponent
Whenever a re-render happens, React looks at this type. If it's the same, then it'll keep the component's state, and re-render it.
Now let's say you render MyComponent during first render, but then changed it to OtherComponent later. React will unmount MyComponent and render OtherComponent. All the local state of MyComponent will be destroyed when it unmounts.
When you create components inline inside another component, every render, there's a new function created, which means a new component type every render. Since the type changes every render, React will unmount previous component and mount the new component every time, which is undesirable.
Take the following example:
const a = { foo: 42 };
function getA() {
return a;
}
getA() === getA(); // the returned `a` is always the same because you defined the object outside `getA`
function getB() {
return { foo: 42 };
}
getB() !== getB(); // the returned object is a new one every time you call `getB` because you defined the object inside `getB`
So it's not that inline functions are evaluated differently, the problem is because every time there's a new component which destroys any local state and even if you don't have local state, unmounting a component and mounting a new one is usually slower than updating the same component.

Categories

Resources