I built a React page like this:
.
The switcher is binded to a callback function from the parent componentA and componentA get this function from top-level page. The callback updates top-level page's state. So when the switcher clicked, then top-level page will rerender the componentA.
The switcher I'm using is a component from React-Switch library: https://www.npmjs.com/package/react-switch
Then sometimes, when I click the switcher, there will be a warning:
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the ReactSwitch component.
Here is some code snippet.
In top-level page:
// In the top-level page, there is a state to decide whether or not to apply some data filter
onSwitcherClicked(event) {
this.setState({
applyFilter: event,
});
render() {
const filter = this.state.applyFilter ? '{paths:["/some_filters"],}' : '{paths:["/none"],}';
return (
<div>
<ComponentA
filter = {filter}
applyFilter = {this.applyFilter}
callBack = {this.onSwitcherClicked}
/>
</div>
);
In Component A
// Component A
componentWillMount() {
// Send some API request according to this.props.filter and load the data
// So every time the switcher clicked, the page will update the filter and pass some new props to componentA,
// then ComponentA will remount it self
}
render() {
return (
<div>
<DataViewer>
{/*A component to display the data*/}
</DataViewer>
<Switch onChange={this.props.callBack}
checked={this.props.applyFilter}/>
</div>
)
}
Here is the error message"
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the ReactSwitch component.
printWarning warning.js:33
warning warning.js:57
React 3
getInternalInstanceReadyForUpdate
enqueueSetState
setState
$onDragStop react-switch.dev.js:252
$onMouseUp react-switch.dev.js:277
I also copied the code in react-switch.dev.js:252 and react-switch.dev.js:277:
// react-switch.dev.js:252
252 this.setState({
253 $isDragging: false,
254 $hasOutline: false
255 });
256 this.$lastDragAt = Date.now();
// switch.dev.js:277
276 ReactSwitch.prototype.$onMouseUp = function $onMouseUp(event) {
277 this.$onDragStop(event);
278 window.removeEventListener("mousemove", this.$onMouseMove);
279 window.removeEventListener("mouseup", this.$onMouseUp);
280 };
I guess it's because when I click the switcher, it will reset the its own status. Meanwhile, the parent component's state is changed too. So when the switcher calls setState(), itself has been unmounted. Am I correct? Is there some approach to fix it?
Thank you!
Too long for a comment, but your component structure should look very similar to this kind of thing, in which case you wouldn't be getting that warning. I have a feeling you might be trying to duplicate the source of truth for state instead of just letting it flow down.
const ParentComponent = () => {
const [isClicked,setIsClicked] = useState(false);
return (
...
<Switcher selected={isClicked} onClick={() => setIsClicked(!isClicked)}/>
)
}
Related
I have a parent Component with a state variable that gets changed by one of its child components upon interaction. The parent then also contains some more components based on the data in the state variable.
The problem is that the child component rerenders when the state of its parent changes because the reference to the setState function changes. But when I use useCallback (as suggested here), the state of my parent just does not update at all.
This is my current setup:
function ArtistGraphContainer() {
const [artistPopUps, setArtistPopUps] = useState([])
const addArtistPopUp = useCallback(
(artistGeniusId, xPos, yPos) => {
setArtistPopUps([{artistGeniusId, xPos, yPos}].concat(artistPopUps))
},
[],
)
return (
<div className='artist-graph-container'>
<ArtistGraph addArtistPopUp={addArtistPopUp} key={1}></ArtistGraph>
{artistPopUps.map((popUp) => {
<ArtistPopUp
artistGeniusId={popUp.artistGeniusId}
xPos={popUp.xPos}
yPos={popUp.yPos}
></ArtistPopUp>
})}
</div>
)
}
And the Child Component:
function ArtistGraph({addArtistPopUp}) {
// querying data
if(records) {
// wrangling data
const events = {
doubleClick: function(event) {
handleNodeClick(event)
}
}
return (
<div className='artist-graph'>
<Graph
graph={graph}
options={options}
events={events}
key={uniqueId()}
>
</Graph>
</div>
)
}
else{
return(<CircularProgress></CircularProgress>)
}
}
function areEqual(prevProps, nextProps) {
return true
}
export default React.memo(ArtistGraph, areEqual)
In any other case the rerendering of the Child component wouldn't be such a problem but sadly it causes the Graph to redraw.
So how do I manage to update the state of my parent Component without the Graph being redrawn?
Thanks in advance!
A few things, the child may be rerendering, but it's not for your stated reason. setState functions are guaranteed in their identity, they don't change just because of a rerender. That's why it's safe to exclude them from dependency arrays in useEffect, useMemo, and useCallback. If you want further evidence of this, you can check out this sandbox I set up: https://codesandbox.io/s/funny-carson-sip5x
In my example, you'll see that the parent components state is changed when you click the child's button, but that the console log that would fire if the child was rerendering is not logging.
Given the above, I'd back away from the usCallback approach you are using now. I'd say it's anti-pattern. As a word of warning though, your useCallback was missing a required dependency, artistPopUp.
From there it is hard to say what is causing your component to rerender because your examples are missing key information like where the graphs, options, or records values are coming from. One thing that could lead to unexpected rerenders is if you are causing full mounts and dismounts of the parent or child component at some point.
A last note, you definitely do not need to pass that second argument to React.memo.
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
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.
I have two pages (page1 and page2) with a SimpleTable component.
I have a page with a table for apples:
render() {
const props = this.props
return (
<Page {...props}>
<SampleTable requestData={this.getApples} columns={[<columns for apples>]} />
</Page>
)
}
And a page with a table for tomatoes:
render() {
const props = this.props
return (
<Page {...props}>
<SampleTable requestData={this.getTomatoes} columns={[<columns for tomatoes>]} />
</Page>
)
}
For reasons unknown to me, this particular child (SampleTable) is not being unmounted / mounted when I transition from page1 to page2 or vice-versa.
It is strange, since all the other children in all the other pages are being mounted / unmounted, even when I am using the same component for the children. So, clearly, I do not understand how / when does React decide to mount / unmount, or when does it decide to reuse a component. But I digress.
I will accept this fact: React will reuse children whenever it pleases.
My problem is that I am using Sampletable.componentDidMount to request data:
componentDidMount() {
console.log('SampleTable did mount. Requesting data ...')
this.props.requestData(state.pageSize, state.page, state.sorted, state.filtered).then(res => {
this.setState({
data: res.rows,
pages: res.pages,
loading: false,
})
})
}
But the function to request the data is provided by the parent, via props. I have the following problem:
on initial rendering, page1 provides a requestData=getApples via props to a SampleTable child.
SampleTable mounts and requests the data, using the getApples method provided by page1
I move to page2
page2 provides a different requestData=getTomatoes method to a SampleTable child
somehow Reacts decides that it can reuse the original component for this child and, since it is already mounted, componentDidMount is not called
and therefore, the new getTomatoes method is not called, and no data suitable for the tomatoes table is requested.
What am I doing wrong?
How can I ensure that the change in props is triggering a data reload?
You should try using a unique key attribute on your SampleData component while calling it, so that react knows it a different instance of your component and re-render it accordingly.
You can find more about keys here: https://reactjs.org/docs/lists-and-keys.html
I'm trying to use the new Context API in my app and it looks like every time I update the context, it re-renders any component connected to it regardless. I have a sandbox demo setup to see code and working issue. When you type in the input - the buttons context is rendered and vice-versa. My original thinking was that if you type in the input, only the input context would be printed out.
DEMO
Is this how it works or am I missing something?
Thanks,
Spencer
The way I avoid re-rendering with react context API:
First I write my component as pure functional component:
const MyComponent = React.memo(({
somePropFromContext,
otherPropFromContext,
someRegularPropNotFromContext
}) => {
... // regular component logic
return(
... // regular component return
)
});
Then I write a function to select props from context:
function select(){
const { someSelector, otherSelector } = useContext(MyContext);
return {
somePropFromContext: someSelector,
otherPropFromContext: otherSelector,
}
}
I have my connect HOC wrote:
function connect(WrappedComponent, select){
return function(props){
const selectors = select();
return <WrappedComponent {...selectors} {...props}/>
}
}
All together
import connect from 'path/to/connect'
const MyComponent ... //previous code
function select() ... //previous code
export default connect(MyComponent, select)
Usage
<MyComponent someRegularPropNotFromContext={something} />
Demo
Demo on codesandbox
Conclusion
MyComponent will re-render only if the specifics props from context updates with a new value, if the value is the same, it will not re-render. Also it avoid re-rendering on any other value from context that is not used inside MyComponent. The code inside select will execute every time the context updates, but as it does nothing, its ok, since no re-rendering of MyComponent is wasted.
That is the expected behaviour. Components as consumers re-renders when their provider data updates. Further more, shouldComponentUpdate hooks do not work on Consumers.
Quoting React's content API:
All Consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. The propagation from Provider to its descendant Consumers is not subject to the shouldComponentUpdate method, so the Consumer is updated even when an ancestor component bails out of the update.
For more info check here