I'm using React 16.8.2, and I'm having a problem with children of my component unmounting whenever state is changed in the app component.
Here's the scenario:
I have App.jsx (a functional component) with a number of state variables (useState)
The setters for some of these state variables are passed down the tree through a Context provider (useContext in the descendent)
I have a menu component (descendent of app), that invokes these setters to (for example) show a modal dialog
I have a modal dialog component (child of App), that uses the state variable as a property to determine whether it is open or not -- standard React stuff I think.
My problem: when any state variables in App are changed (through hooks of course), the children of App are unmounted and remounted- even if they have no connection to the state being changed. They aren't just re-rendered - the children are unmounted and their state is re-initialized. So the fields are cleared on my dialog, when they shouldn't be, for example.
This is already a fairly complex application, so I've spent a lot of time today isolating the problem. I then set up a simple create-react-app to try to replicate this behavior there - but this test app behaves like it should. Changing parent state, whether through a prop callback, or through a context-provided callback from the child - re-renders but does not unmount/remount and child state remains intact.
But in my real app, the components re-mount and child state gets re-initialized.
I've simplified it down to the barest that I can - I'm setting a fake state variable "foo" with "setFoo" through the Context from the child. Even though foo is not used by any component, changing the value of foo causes the children of App to unmount/remount.
In App.jsx:
const App = props => {
const [foo, setFoo] = useState(false);
// ...
const appControl = {
toggleFoo: () => setFoo(!foo);
};
// ...
return (
<AppContext.Provider value={appControl}>
... a bunch of stuff not using foo anywhere
... including, deep down:
<Menu />
</AppContext.Provider>
);
};
In Menu.jsx:
const Menu = props => {
const appControl = useContext(AppContext);
// ...
return (
... super simplified for test
<div onClick={appControl.toggleFoo}>
Toggle Foo
</div>
);
};
If I understand state properly, I do believe that changing state should result in children being re-rendered, but not re-mounted. This is what I'm seeing in my simple create-react-app test, but not in my real app.
I do see that I'm not on the latest React - perhaps upgrading will fix this?
Thanks for any insight on what I may be doing wrong, or misunderstanding here.
Solved. This is an interesting one. Here's what happened.
In my App component, I had a fairly deep tree of HOC's. Due to some dubious decisions on my part, I ended up breaking App into two components. App and AppCore. I had a reason for it, and it seemed to make sense at 3am. But to be both quick and dirty, I stuck AppCore as a const, inside my App function. I remember thinking to myself "I wonder what problems this will cause?" Now I know. Perhaps a React expert can fully explain this one to me though, as I don't see the difference between JSX assigned to a constant, and JSX returned directly. But there clearly is, and this is simple to reproduce.
To reproduce, create-react-app a test app:
create-react-app test
cd test
Then replace the contents of App.js with:
import React, { useState, useEffect } from "react";
const Menu = props => <div onClick={props.click}>Toggle Foo</div>;
const Test = props => {
useEffect(() => {
console.log("mounted");
return () => console.log("unmounted");
}, []);
return null;
};
const App = props => {
const [foo, setFoo] = useState(false);
// this is the root of the problem
// move this line outside of the function body
// and it mounts/unmounts correctly
const AppCore = props => <Test />;
return (
<>
<Menu click={() => setFoo(!foo)} />
<AppCore />
</>
);
};
export default App;
Then npm start, and when you click on "Toggle Foo" you'll see that the Test component is unmounted/remounted.
The solution here, is to simply move AppCore out of the function body. In my real app, this means I have some refactoring to do.
I wonder if this would be considered a React issue?
Related
I'm trying to understand how React "displays and update" the code below, assuming I've understood the differences and the vocabulary explained here
https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html
import React from "react";
export default function App() {
console.log("App is rerendered")
const [time, setTime] = React.useState(0)
React.useEffect(() => {
const lol = setTimeout(() => setTime(prev => prev + 1), 100)
return () => clearTimeout(lol)
}, [time]
)
function ShowTime() {
console.log("ShowTime is rerended")
return (
<div> Time : {time / 10} sec</div>
)
}
function ShowButton() {
console.log("ShowButton is rerended")
return (
<button
onClick={() => console.log("I'm hard to click cuz rerendered the whole time :/")}>
Button created with ShowButton component
</button>
)
}
return (
<main>
<ShowTime />
<ShowButton />
</main>
)
}
React create the virtual dom with the App element, the ShowTime element, and the ShowButton element inside
It's the first render so React renders everything, creating an instance of App, containing a main DOM element, containing one instance of ShowTime and one instance of ShowButton
After 100ms, time state in App changed !
React update the virtual dom again taking account time state has changed
It's rerendering, so there is reconciliation
https://reactjs.org/docs/reconciliation.html#component-elements-of-the-same-type
says "When a component updates, the instance stays the same (...). Next, the render() method is called (...)"
React does't care if App changed or not. It's a component, and when he encounters a component in the virtual dom, when commiting, the instance stays the same, and React runs App.render()
In this case it's nice, because time state has changed.
Recursing process of reconciliation on children
In the same way, React does't care if ShowTime and ShowButton changed or not. They're components, so React keeps their instance, runs ShowTime.render() and ShowButton.render()
My two questions :
Is my understanding of the reconciliation process (concerning the components part) is right ?
So a component inside a component that has to be rendered will be rendered, whatever if it is concerned about any props or state changes or not ? (it's the case of my ShowButton component)
That's weird no ? Because of that it's very hard to click it !
The declaration of ShowButton is right inside the App render function. So React not only rerender it but inserts a new DOM button element 10 times a second. That's why it is hard to click. Move ShowButton out of App.
Reconciliation happens after all rendering.
In some cases React doesn't rerender components. We use React.memo, PureComponent and shouldComponentUpdate for the optimization. For more information please read this answer.
There are ShowTime.render() and ShowButton.render() in your text but functional components doesn't have methods.
In the same way, React does't care if ShowTime and ShowButton changed or not. They're components, so React keeps their instance, runs ShowTime.render() and ShowButton.render()
React doesn't care, as long as it's the same component. The problem is that ShowTime and ShowButton are a new component with the same name, created every time App is rerendered. It's like saying
const ShowTime = () => {
// ... (btw, don't do this either)
}
So while the ultimate structure is the same, React sees new components every rerender of App.
To solve this problem, pull the components out of App:
function ShowTime() { ... }
function ShowButton() { ... }
function App() { ... }
import React from 'react';
const App = () => {
console.log('app render');
return (
<div>
<Children />
</div>
);
};
const Children = () => {
const [update, setUpdate] = React.useState(false);
console.log('children render');
return (
<div>
<button onClick={() => setUpdate(!update)}>update</button>
</div>
);
};
export default App;
Above code, at first, two messages are printed out 'app render' and 'children render.'
Then, when I click the update button, a message is printed 'children render'.
I learned the following rules when I started to learn React at beginning.
when props are updated
when states are updated
when the parent component is updated
I've been working on React Projects well since then.
Today, somehow, I got a question.
How does react know which components have been updated?
setUpdate tells React that Children is updated?
I think I can understand that React can notice the props changing.
because, if render functions are called, it means they can know whether props of their children are changed or not.
But I don't understand how react recognizes state-changing.
The components form a tree. In the first render, the React framework renders from the root node. Any component functions that call useState will have the set state function registered and associated with the component instance. In the future, if you call a set state function, the React framework finds the associated component instance to re-render the sub-tree.
Is there any special condition, that causes a react component to reset completely (not just re-render)?
What do I mean by reset?
Reverting back ALL STATES to their default values (that given in useState()) and re-running all my useEffect() callbacks (even with empty dependency-list ([])) … LIKE ANOTHER OR A NEW COMPONENT!
This wonderful event happens for one of my components during its lifetime (after a lot of correct re-rendering for this component and other components). I don't know why! And I thought this is an impossible event in React:
export default function Project (props) {
const [temp, setTemp] = useState(false)
const tempRef = useRef(false)
console.debug({ temp }, tempRef)
useEffect(() => {
console.debug('INITIALIZATION:', { temp }, tempRef)
setTemp(true)
tempRef.current = true
}, [])
// ...
}
I'm suspicious of react-router-dom#6.0.0-beta.8 that I used in my project. But I can't change it, because of the scale of the project and a lot of usage of it.
UPDATE: All parents of this component are working as expected. Even after this event (for this component).
Not so fluent with React hooks, used plenty of class components before, hope you'll be forgiving.
The current code causes infinite re-rendering, and I think I understand why - the entire function body is being called on re-render.
const NavTabs = () => {
const classes = useStyles();
const [categories, setCategories] = React.useState();
const axiosPromise = getRequest(consts.categoriesURL);
axiosPromise.then(data => {
setCategories(data.value);
})
return (
<div className={classes.root}>
<AppBar position="static">
</AppBar>
{categories && <DynamicTabs categories={categories}/>}
</div>
);
}
I guess I could do something like if (!categories) { const axiosPromise [...] and so forth, i.e. do the http request only if categories haven't been populated yet. I guess this could also be solved by useEffect? Or wrapping the hook in an internal function?
I guess my real question is - why is React re-rendering the entire function body? Shouldn't it re-render only the return function? And then what is the point of using hooks that will be re-run on every render?
Compared to class components - shouldn't the code in the function body be equivalent to the constructor code in class components, and the return function - equivalent to the render method?
I guess I could do something like if (!categories) { const axiosPromise [...] and so forth, i.e. do the http request only if categories haven't been populated yet. I guess this could also be solved by useEffect? Or wrapping the hook in an internal function?
Yes, useEffect is the way to go here. Making a request and setting the result as state are side effects should only be run once in your case. We can achieve that easily with useEffect.
I guess my real question is - why is React re-rendering the entire function body? Shouldn't it re-render only the return function? And then what is the point of using hooks that will be re-run on every render?
React has no way to split a js function and only re-render the return. The function is atomic and must be completed. That is what hooks are for. React controls when hooks are ran so it can do fun stuff like batch state updates, ignore outdated effects and prioritise high priority work like animations.
Compared to class components - shouldn't the code in the function body be equivalent to the constructor code in class components, and the return function - equivalent to the render method?
The functional component is equivalent to the render method of a class component. They are called in a similar way. All the other lifecycle methods are replaced by hooks.
I recommend the react docs are great place to start and Dan Abramov has a great deep dive on hooks.
Yes, getRequest is being invoked each render cycle which sets some state and triggers a rerender. Placing it in an effect hook with a dependency array is likely the best solution. What dependencies you define will dictate when getRequest can be invoked.
Why is React re-rendering the entire function body?
The entire function body needs to run in order to determine the return value.
And then what is the point of using hooks that will be re-run on every render?
Hooks are run on every render, in the same order they are defined, but depending on dependencies may not invoke a callback. Hooks are what give functional components so much viability and sense of component lifecycle, to nearly be equivalent to class-based components in functionality. In most cases, you can completely convert a class-based component to a functional one and not drop any functionality.
Compared to class components - shouldn't the code in the function body be equivalent to the constructor code in class components, and the return function - equivalent to the render method?
It is more accurate to think of the entire functional components definition as the class-based render function, which can contain some logic and returns computed JSX to render to the DOM.
Example Solution:
const NavTabs = () => {
const classes = useStyles();
const [categories, setCategories] = React.useState(); // <-- no initial state!
useEffect(() => {
getRequest(consts.categoriesURL).then(data => {
setCategories(data.value); // <-- will update state and trigger render
});
}, []); // <-- empty dependency is run once on component mount
return (
<div className={classes.root}>
<AppBar position="static">
</AppBar>
{categories && <DynamicTabs categories={categories}/>}
</div>
);
}
To answer "why react is running the entire function" the answer is that javascript functions work that way: you always have to run the whole thing, they don't stop in the middle*. I understand what you are thinking here, if you are used to class components: don't I have a constructor section and a render section? and the answer is: not really if you are using function components. You only have render. But hooks are magic, and they let you pretend to have two parts.
Hooks know when they are called, and assuming you always call them in the same order, the can keep track of state outside the render function. so the way the work is sorta like this:
React detects a function component and creates or re-uses an existing rendering context for that component. This is where the hook information lives.
React calls your function component and it starts running.
You call hooks within your function component. These check what the current rendering context is, and save/get relevant information from that context. In a sense the rendering context is a "global" variable.
You do whatever else you want within the function, and eventually return a component tree (JSX) or null.
react then (eventually) updates the DOM to match what you returned, and saves the changes to the rendering context, so the next time render is called, it can re-use the context.
The magic is that the rendering context can do fancy things with hooks, like only run them once, always return the same value from a hook, or any other number of things. But in a sense, the component "class" becomes the react-internal rendering context that hooks know how to access.
Here is an example of the useState hook implemented in a class component: (You wouldn't ever need to do this, but it's an example of how hooks work).
class FakeHook extends React.Component {
constructor(...args) {
super(...args)
this.state = {}
this.useStateCalls = 0
}
useState(defaultValue){
const currentRenderContext = this.state
let value = defaultValue
const currentStateKey = `useState${this.useStateCalls}`
if (currentStateKey in currentRenderContext) value = currentRenderContext[currentStateKey]
this.useStateCalls++
return[value, (newValue) => this.setState({[currentStateKey]: newValue})]
}
render(){
this.useStateCalls = 0
let [fooState, setFoo] = this.useState("foo default")
let [barState, setBar] = this.useState("bar default")
return(
<dl>
<dt>Foo state</dt>
<dd>
<strong>Value:</strong>
<div>{fooState}</div>
<button onClick={(event) => {event.preventDefault(); setFoo(`foo updated at ${new Date().toLocaleString()}`)}}>Update Foo</button>
</dd>
<dt>Bar state</dt>
<dd>
<strong>Value:</strong>
<div>{barState}</div>
<button onClick={(event) => {event.preventDefault(); setBar(`bar updated at ${new Date().toLocaleString()}`)}}>Update Bar</button>
</dd>
<dt>Render context state:</dt>
<dd><pre>{JSON.stringify(this.state)}</pre></dd>
</dl>
)
}
}
ReactDOM.render(<FakeHook/>, document.getElementById('main'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<main id=main>loading or error occurred...</main>
Notice that state is stored based on the order the hook is called inside render. In real hooks, the render context is stored somewhere other than this.state, but hooks know how to get it, and you don't really care. Also, this is just an example, real hooks work slightly differently, but the concept is the same.
*: async functions and generators don't run all at once, and instead return a special object that lets the function run in multiple steps, waiting or pausing on await or yield.
Am getting this warning:
Can't perform a React state update on unmounted component. This is a no-op...
It results from a child component and I can't figure out how to make it go away.
Please note that I have read many other posts about why this happens, and understand the basic issue. However, most solutions suggest cancelling subscriptions in a componentWillUnmount style function (I'm using react hooks)
I don't know if this points to some larger fundamental misunderstanding I have of React,but here is essentially what i have:
import React, { useEffect, useRef } from 'react';
import Picker from 'emoji-picker-react';
const MyTextarea = (props) => {
const onClick = (event, emojiObject) => {
//do stuff...
}
const isMountedRef = useRef(true);
useEffect(() => {
isMountedRef.current = true;
});
useEffect(() => {
return () => {
console.log('will unmount');
isMountedRef.current = false;
}
});
return (
<div>
<textarea></textarea>
{ isMountedRef.current ? (
<Picker onEmojiClick={onClick}/>
):null
}
</div>
);
};
export default MyTextarea;
(tl;dr) Please note:
MyTextarea component has a parent component which is only rendered on a certain route.
Theres also a Menu component that once clicked, changes the route and depending on the situation will either show MyTextarea's parent component or show another component.
This warning happens once I click the Menu to switch off MyTextarea's parent component.
More Context
Other answers on StackOverflow suggest making changes to prevent state updates when a component isn't mounted. In my situation, I cannot do that because I didn't design the Picker component (rendered by MyTextarea). The Warning originates from this <Picker onEmojiClick={onClick}> line but I wouldn't want to modify this off-the-shelf component.
That's explains my attempt to either render the component or not based on the isMountedRef. However this doesn't work either. What happens is the component is either rendered if i set useRef(true), or it's never rendered at all if i set useRef(null) as many have suggested.
I'm not exactly sure what your problem actually is (is it that you can't get rid of the warning or that the <Picker> is either always rendering or never is), but I'll try to address all the problems I see.
Firstly, you shouldn't need to conditionally render the <Picker> depending on whether MyTextArea is mounted or not. Since components only render after mounting, the <Picker> will never render if the component it's in hasn't mounted.
That being said, if you still want to keep track of when the component is mounted, I'd suggest not using hooks, and using componentDidMount and componentWillUnmount with setState() instead. Not only will this make it easier to understand your component's lifecycle, but there are also some problems with the way you're using hooks.
Right now, your useRef(true) will set isMountedRef.current to true when the component is initialized, so it will be true even before its mounted. useRef() is not the same as componentDidMount().
Using 'useEffect()' to switch isMountedRef.current to true when the component is mounted won't work either. While it will fire when the component is mounted, useEffect() is for side effects, not state updates, so it doesn't trigger a re-render, which is why the component never renders when you set useRef(null).
Also, your useEffect() hook will fire every time your component updates, not just when it mounts, and your clean up function (the function being returned) will also fire on every update, not just when it unmounts. So on every update, isMountedRef.current will switch from true to false and back to true. However, none of this matters because the component won't re-render anyways (as explained above).
If you really do need to use useEffect() then you should combine it into one function and use it to update state so that it triggers a re-render:
const [isMounted, setIsMounted] = useState(false); // Create state variables
useEffect(() => {
setIsMounted(true); // The effect and clean up are in one function
return () => {
console.log('will unmount');
setIsMounted(false);
}
}, [] // This prevents firing on every update, w/o it you'll get an infinite loop
);
Lastly, from the code you shared, your component couldn't be causing the warning because there are no state updates anywhere in your code. You should check the picker's repo for issues.
Edit: Seems the warning is caused by your Picker package and there's already an issue for it https://github.com/ealush/emoji-picker-react/issues/142