Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Component unmounts when parent state changes

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.

like image 594
Eric S Avatar asked Apr 10 '19 07:04

Eric S


People also ask

Does child component Rerender on parent state change?

No, it will not re-render. If you pass any props to the component from the parent component and you update that prop in children or that prop update in the parent component so both will re-render.

Can we change state of parent component from child component in React?

Finest Laravel Course - Learn from 0 to ninja with ReactJS To update the parent state from the children component, either we can use additional dependencies like Redux or we can use this simple method of passing the state of the parent to the children and handling it accordingly.

How can component be passed from parent to child?

To pass data from child to parent component in React:Pass a function as a prop to the Child component. Call the function in the Child component and pass the data as arguments. Access the data in the function in the Parent .

What does React do in response to a component's state changing?

To make the state change, React gives us a setState function that allows us to update the value of the state. Calling setState automatically re-renders the entire component and all its child components.


1 Answers

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?

like image 95
Eric S Avatar answered Sep 30 '22 18:09

Eric S