Why Does React Component Unmount and remount on state change? - javascript

Why does my HomeTypes component unmount and remount every time I use setDrawerOpen?
Does a state change inside a component always cause that component to unmount and then re-mount?
import React, { useEffect, useState } from 'react';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import Drawer from '../../components/DrawerComponent/Drawer';
function HomeTypes() {
const [drawerOpen, setDrawerOpen] = useState(null);
useEffect(() => {
console.log('Home Types Mounted');
return () => {
console.log('Home Types Unmounted');
};
});
return (
<div className={`dashboard-page`}>
<h1 className="dashboard-page-title">
Home Types
<button
className={`btn bright primary`}
onClick={() => {
setDrawerOpen(true);
}}>
<FontAwesomeIcon icon={['fas', 'plus']} />
<span>add home type</span>
</button>
</h1>
<Drawer
drawerOpen={drawerOpen}
closeDrawer={() => {
setDrawerOpen(false);
}}
title="Add Home Type"
drawerContent="Hello World"
/>
</div>
);
}
export default HomeTypes;

You're missing a dependencies array in your useEffect, that might be causing the behavior
useEffect(() => {
console.log('Home Types Mounted');
return () => {
console.log('Home Types Unmounted');
};
}. []); // this array right here

documentation see the Note in orange below: Using the Effect Hook
The hook useEffect will run every time the component renders.
to run the effect once on mount (end cleanup on dismount) run it with a second parameter [] in this form:
useEffect(() => {
// Side Effect
return () => {
// Cleanup
}
}, [])
Edit: Better to just quote the docs
Note
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders. Learn more about how to deal with functions and what to do when the array changes too often.
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.
We recommend using the exhaustive-deps rule as part of our eslint-plugin-react-hooks package. It warns when dependencies are specified incorrectly and suggests a fix.

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;

Why does React render component for the second time after setting the state to the same value?

I have a simple React component and I set the same value each time that I click on the button:
import React, { useState } from 'react';
import './style.css';
let data = { title: 'ABC' };
export default function App() {
const [foo, setFoo] = useState();
console.log('Rendered');
return (
<div>
<button onClick={() => setFoo(data)}>Set Data</button>
<h1>Data: {JSON.stringify(foo)}</h1>
</div>
);
}
But renders are a little bit strange because at the second button click React renders the component but I set the same value.
Why React re-render the component although I set the same value?
Demo Here
The question is about why the component renders although the new state equals the previous state (shallow comparison)
// On second button click
const prevState = data
// State trigger
setFoo(data)
// Same state, it doesn't triggers a render
data === prevState
So, the component didn't trigger render due to state change.
But it happened due to another reason, as mentioned in docs "between the lines" under Hooks API Bailing out of a state update section:
Note that React may still need to render that specific component again before bailing out.
Unlike in class component, for function components, after setting the same state like in our case, sometimes, React will need another render to validate its equality. Its unfortunate edge case.
But it should not consider you as "performance issue" since it does not effect the React.Node tree, it won't continue in the reconciliation process if the state didn't change. It even won't make unnecessary hooks calls.
Another Simplified Example
Same goes here, there is another render for bail out, another log of "A".
Although there is a "bail out" render, notice that the useEffect does not run.
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
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>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
If you wondering on logs order ("Why 'A' logged before 'B'?"), try deep diving another question: React useEffect in depth / use of useEffect?
In functional component, a component isn't re-rendered if it's the same value, i.e. a value that passes === comparison:
const [state, setState] = useState({});
...
setState(state => state); // no re-render
Otherwise a component is re-rendered:
setState(state => ({...state})); // re-render
As commented already, it may be "rerendered" by react, but the DOM will likely not change.
Checkout https://github.com/facebook/react/issues/17474

Can you control the re-renders of a functional react component based on state change?

I have a basic e-commerce app for practice. There's a functional component called "Shop" which has two states:
[products, setProducts] = useState([10ProductObjects])
and [cart, setCart] = useState([])
On the first render cycle, the 10 products are loaded and each Product component has an "add to cart" button that adds a product to the cart array.
When I click the button, the cart array gets populated and its length is displayed. However, doing so re-renders the 10 products again even though nothing has been changed on them.
Now as I see it since one of the states changes i.e. the cart array, the whole Shop component is rendered again. Which in turn renders its child components, including those which were not changed.
I've tried to use React.memo but it doesn't help since no props are being changed on "Shop", rather the state is changing. I've also used the useMemo hook and it had some interesting results.
Using the products array as a dependency solves the extra re-rendering problem, but the new products are not added to the cart anymore. Using both [products, cart] as the dependencies works but brings back the original problem.
I know it could be done using shouldComponentUpdate but I need that kind of flexibility in functional components.
N.B: This is my first ever question and I'm all ears for any kind of feedback.
import React, { useState, useMemo } from 'react';
import fakeData from '../../fakeData';
import Product from '../Product/Product';
const Shop = () => {
console.log('[Shop.js] rendered')
const first10 = fakeData.slice(0, 10);
const [products, setProducts] = useState(first10);
const [cart, setCart] = useState([]);
const addProductHandler = (product) => {
console.log(cart, product);
const newCart = [...cart, product];
console.log(newCart);
setCart(newCart);
}
let productsOnScreen = useMemo(() => {
return products.map( prod => {
return <Product product={prod} addProductHandler={addProductHandler} />
});
}, [products])
return (
<div className="shop-container">
<div className="product-container">
{productsOnScreen}
</div>
<div className="cart-container">
<h3>this is cart</h3>
<h5>Order Summary: {cart.length}</h5>
</div>
</div>
);
};
export default Shop;
Memoize addProductHandler with React.useCallback so that the reference to it does not change between renders:
const addProductHandler = React.useCallback((product) => {
setCart(oldCart => {
return [...oldCart, product];
});
}, [setCart]);
Then, memoize <Product> with React.memo. You didn't post your code for that component but it would look something like this:
export const Product = React.memo((props) => {
// normal functional component stuff
return <>product component or whatever</>;
});
These are both necessary for a component to avoid unnecessary rerenders. Why?
React.useCallback allows for comparison-by-reference to work for a callback function between renders. This does not work if you declare the callback function every render, as you have currently.
React.memo wraps a component to enable it to render or not render depending on a shallow comparison of its props. React components do NOT do this by default, you have to explicitly enable it with React.memo.
As the docs mention:
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.
In other words, you should only use this as a performance optimization if you are experiencing slowdowns, NOT to prevent renders for any other reason, and ONLY if you are actually experiencing performance issues.
If this doesn't work, it's likely that one of your <Product> props is still changing (in the context of shallow comparison-by-reference) between renders. Keep playing with and memoizing props until you figure out which ones are changing between renders. One way to test this is a simple React.useEffect (for debug purposes only) inside of <Product> which will alert you when a prop has changed:
React.useEffect(() => {
console.log('product prop changed');
}, [product]);
React.useEffect(() => {
console.log('addProductHandler prop changed');
}, [addProductHandler]);
// etc...

how choice when useEffect is executed in React

I try to understand how useEffect() work in React, and why is executed in the last not in the order of my code. This issue causes a problem in my bigger program. My question : There is a possibility to choice when this one is executed ?
log of the console, like you see, is not of the order of print :(
0 children null App.js:11:10
2 children null App.js:16:10
1 children
Array [ "truc" ]
https://codesandbox.io/s/busy-cookies-yvq16?file=/src/App.js
I make this snippet code to explain my problem
import "./styles.css";
import { useEffect } from "react";
let list = [];
function Buffer() {
let init = null;
list.push(init);
return <Comp>{list[0]}</Comp>;
}
function Comp({ children }) {
console.log("0 children", children);
useEffect(() => {
children = ["truc"];
console.log("1 children", children);
}, []);
console.log("2 children", children);
return <div>Super Buffer</div>;
}
export default function App() {
return (
<div className="App">
<Buffer />
</div>
);
}
why is executed in the last not in the order of my code.
Because that's its purpose, that's what it's for. From the documentation:
useEffect
useEffect(didUpdate);
Accepts a function that contains imperative, possibly effectful code.
Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as React’s render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.
Instead, use useEffect. The function passed to useEffect will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.
This article by Dan Abramov may also be useful: A Complete Guide to useEffect. Despite the title, it doesn't just cover useEffect, but the whole way hooks and function components work.
There is a possibility to choice when this one is executed ?
It's not really clear from your question what you want to do instead, but if you want to do something right away, just do it right away without any wrapper function. But remember that your component function runs every time the component needs to re-render, so useEffect is what you use to run things only A) When the component first mounts (and optionally when it unmounts) or B) When certain things change.
children = ["truc"];
That's very suspect code, since children is a prop. You shouldn't be assigning to props (not even destructured copies of them). Props are state controlled by the parent component. The component should only use them, not try to directly modify them.
Just in case it helps, render takes place from children to parent components. In case you need to handle some execution in the order wanted, you can use the parent's useEffect like this:
import "./styles.css";
import { useEffect, useState } from "react";
let list = [];
function Buffer() {
useEffect(() => {
console.log("2 children", list[0]); //log moved here
}, []);
let init = null;
let children = list.push(init);
return <Comp>{list[0]}</Comp>;
}
function Comp({ children }) {
console.log("0 children", children);
useEffect(() => {
children = ["truc"];
console.log("1 children", children);
}, []);
return <div>Super Buffer</div>;
}
export default function App() {
return (
<div className="App">
<Buffer />
</div>
);
}
OutPut:
0 children null
1 children (1) ["truc"]
2 children null
To log the children in the parent you would need to define the children variable in the parent and wont need to pass that down the prop, for this sample case at least
your useEffect() has a dependency array which specifies when to rerender
it is the empty array in your code. When it is empty, it only renders once your compnent renders. If you want to make it render when something changes, you need this dependency inside of that array
useEffect(() => {
children = ["truc"];
console.log("1 children", children);
}, [children]);
https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect

React component render twice using useState

I'm having a really hard time to figure out what's happening when there is nothing being used to trigger re-render the component.
Events.js Component renders twice when I remove the useState() from the Event.js it renders once, but I need to keep it. when I use useEffect() inside Event components, renders fourth time.
I just kept the dummy data to give you to fill the emptiness and tried to remove React.memo, nothing happens. the problem is with the Event.js component I believe. I'm also using the Context API, but forth time rendering is too much.
useEffect inside App.js is getting some value from the localStorage, I can't access that direct 'cause the value is undefined by default
sandbox code here: https://codesandbox.io/s/event-manager-reactjs-nbz8z?file=/src/Pages/Events/Events.js
The Events.js file is located on /Pages/Events/Events.js
example code is below
Event.js ( child component )
function Events() {
// Sate Managing
const [allEvents, setAllEvents] = React.useState(null);
console.log('Rendering EventsJs', allEvents);
React.useEffect(() => {
setAllEvents(['apple', 'banana']);
}, []);
return (
<div className="events">
{ console.log('Event Rendered.js =>') }
</div>
)
}
export default React.memo(Events, (prevProps, nextProps) => {
return true;
} );
App.js ( parent component )
import { BrowserRouter, Route, Redirect } from 'react-router-dom';
function App() {
const [userId, setUserId] = React.useState(null);
React.useEffect(() => {
setUserId(1);
}, []);
// Login
return (
<BrowserRouter>
<Navigation />
<Route path='/events' component={Events} />
{console.log('App Rendered')}
</BrowserRouter>
);
}
export default App;
Error:
Your app is working fine. It is rendering as it should. As we know:
A React component re-renders whenever its props or state change.
And react component lifecycle order is:
Initial props/state --> render --> DOM update --> mounted
props/state changed --> render --> DOM update --> updated ... so on
In the example below, it is rendering 2 times and that's correct:
First one (first console.log) is due to initial render with state as []
Second one (second console.log) is due to state change (caused by useEffect) to ['apple', 'banana']
function Events() {
const [allEvents, setAllEvents] = React.useState([]);
console.log('Event Rendered', allEvents);
useEffect(() => {
setAllEvents(['apple', 'banana']);
}, []);
return <>Events</>;
}
About using React.memo:
React.memo only checks for props changes. If your function component wrapped in React.memo has a useState or useContext Hook in its implementation, it will still rerender when state or context change.
You can not skip re-render using React.memo due to change in state. You can only optimize to skip re-rendering caused by change in props.
But in the example above, you don't have props passed from the parent component, the only props passed to Events are those passed by react-router i.e. route props. So, there is no need to use React.memo.
Here is sandbox, check the console.logs. You will see only 3 logs: "App render", "Event render with initial state", "Event render with new state".
EDIT:
If we remove StrictMode from index.html, and add below console.logs in components:
App.js --> console.log('App rendered')
Evenets.js --> console.log('Event rendered', allEvents, isLoading) // (allEvents and isLoading are state variables here)
And go to http://localhost:3000, we see 1 log:
App Rendered
Now click on "Events", we see 3 logs:
1: Event Rendered, [], true
2: Event Rendered, [{}, ... 54 items], true
3: Event Rendered, [{}, ... 54 items], false
which is correct behavior (refer lifecycles order written above):
1st log: render with initial state ([], true)
2nd log: render with new allEvents (54 items) and old isLoading (true)
3rd log: render with old allEvents (54 items) and new isLoading (false)
Below are the right questions to ask now:
Question1:
Why 2nd and 3rd render (log) are separate, should not they be batched (merged) and applied together as they are written in the same function?
fetch('url').then(() => {
// ... code here
setAllEvents([...events])
setLoading(false)
})
Answer:
No, they will not be batched in above code. As explained by Dan Abramov:
This is implementation detail and may change in future versions.
In current release, they will be batched together if you are inside a React event handler. React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.
With current version, several setStates outside of event handlers (e.g. in network responses) will not be batched. So you would get two re-renders in that case.
There exists a temporary API to force batching. If you write ReactDOM.unstable_batchedUpdates(() => { this.fn1(); }); then both calls will be batched. But we expect to remove this API in the future and instead batch everything by default.
So, you can write (inside fetch's then), if you want, it will save 1 render:
ReactDOM.unstable_batchedUpdates(() => {
setAllEvents([...events])
setLoading(false)
})
Question2:
What's React event handler in above quote?
Answer: foo in example below. These 2 set states will be batched.
const foo = () => {
setAllEvents([
{ _id: '5ede5af03915bc469a9d598e', title: 'jfklsd', },
])
setLoading(false)
}
<button onClick={foo}>CLICK</button>
Question3:
Does it update HTML DOM as many times as it renders (prints console.log)?
Answer: No. React compares calculated virtual DOMs before updating real DOM, so only those changes are applied to real DOM which are required to update the UI.
Question4:
Why was rendering doubled when we use StrictMode?
Answer: Yes, StrictMode will intentionally double invoke "render" and some other lifecycle methods to detect side-effects. Strict mode checks are run in development mode only; they do not impact the production build.
Well actually this is caused by your usage of React.memo, its second parameter is called areEqual, and you pass in () => false, so you are basically telling React that the props are always changing. Therefore whenever App rerenders, Events rerenders too.
You should let React.memo check for prop changes. By passing () => false you are actually telling that its props always change (they are never equal).
export default React.memo(Events);
Here's a working example.

Categories