Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does react-router keep components from unmounting with route changes?

I will preface this by saying that everything works correctly, it's just that I'm trying to understand what is happening exactly and why I'm seeing this behavior.

Say I have a component, FooComponent, and two different routes, /foo and /bar. I'm rendering them as follows:

<Route
    exact
    path="/foo"
    render={({ location }) => <FooComponent location={location} />}
/>
<Route
    path="/bar"
    render={({ location }) => <FooComponent location={location} />}
/>

When clicking between pages /foo and /bar, it seems that FooComponent mounts only once. To test this, here's how the FooComponent is implemented:

const FooComponent = ({ location }) => {
  useEffect(() => {
    console.log("Mount");
    return () => {
      console.log("Cleanup");
    };
  }, []);
  return <div>At {location.pathname}</div>;
};

Mount is only logged once.

This completely puzzles me as FooComponent is rendered by two completely different parent Route components, and I thought React would only perform reconciliation on same parent component re-rendering with different child elements. Moreover, changing one to return <div><FooComponent location={location} /></div> and leaving the other as-is will cause the component to unmount.

Here is the reproducible example.

like image 324
rb612 Avatar asked Nov 01 '25 06:11

rb612


1 Answers

Like what you mentioned, I believe this behavior is mostly related to React Reconciliation process. Whenever the root elements have different types, in our case <div /> vs <FooComponent />, React will create a new tree. That's why we are seeing the unmount event.

In the following case, the prop (location) for FooComponent will be changed when we go to a different route. Because we have two React DOM elements of the same type, React will only update and re-render the component.

// location is changed by react-route
<Route
    exact
    path="/foo"
    render={({ location }) => <FooComponent location={location} />}
/>
<Route
    path="/bar"
    render={({ location }) => <FooComponent location={location} />}
/>


const FooComponent = ({ location }) => {
  useEffect(() => {
    console.log("Mount");
    return () => {
      console.log("Cleanup");
    };
  }, []);
  
  // the parent components is the same, only the child node of the following div is chnaged, so React rerenders the div.
  return <div>At {location.pathname}</div>;
};

On the react-router side, Route doesn't change the DOM structure, if you look at the source code. it only acts as a context provider.

return (
            <RouterContext.Provider value={props}>
              {props.match
                ? children
                  ? typeof children === "function"
                    ? __DEV__
                      ? evalChildrenDev(children, props, this.props.path)
                      : children(props)
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? __DEV__
                  ? evalChildrenDev(children, props, this.props.path)
                  : children(props)
                : null}
            </RouterContext.Provider>
          );

References:

react-dom/Router.js

React/Reconciliation

like image 186
Andrew Zheng Avatar answered Nov 03 '25 12:11

Andrew Zheng