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

Viewed 1801

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.

2 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

basically I think that React optimizes on the basic concept that two equal stateless components are equivalent in computing the DOM output. Changing key prop, you can explicit specify that the components should be remounted:

<Route exact path="/foo" render={({ location }) => <FooComponent key='1' location={location} />} />
<Route path="/bar" render={({ location }) => <FooComponent key='2' location={location} />} />
Related