molefrog / wouter

🥢 A minimalist-friendly ~2.1KB routing for React and Preact

Home Page:https://npm.im/wouter

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Components being unnecessarily remounted

chiel opened this issue · comments

Hi there, I'm doing a POC for work to see if wouter is the library we want to move (from react-router which we have a bunch of issues with).

One issue I'm currently running into, which we have with react router as well in some circumstances, is that elements get re-mounted seemingly unnecessarily.

For example:

<Switch>
  <Route path="a">
    <ComponentA />
  </Route>
  <Route path="b">
    <ComponentB />
  </Route>
</Switch>

Where ComponentA and ComponentB both look something like this (the nesting of div > Navbar is the same between them:

function ComponentA() {
  return (
    <div>
      <Navbar />
      <p>Component A</p>
    </div>
  )
}

And Navbar looks something like this:

function Navbar() {
  useEffect(() => {
    return () => {
      console.log('NAVBAR UNMOUNT');
    }
  }, []);

  return null;
}

Whenever switching from route /a to route /b, the Navbar gets unmounted, and remounted, seemingly unnecessarily. Usually, React will reconcile the difference between these components and realise that the navbar does not need to be un/re-mounted and just leave it be. I've tried adding key props at various levels to try help the situation, but to no avail.

Do you have any clues what might be causing this?

Just to confirm, your app is not in StrictMode, right?

Nope! We are actually still on react 16.8, if that matters.

I see, this looks like a standard behaviour: React sees that <ComponentA /> and <ComponentB /> elements are different and it unmounts the first one and mounts the new one. Can you run the following experiment: replace your switch with a simple state like so:

const [value, setValue] = useState("a")

<>
  {value === "a" ? <ComponentA /> : <ComponentB />}
</>

I expanded it a little bit with also adding a toggle in the respective components, and it indeed exhibits the same behaviour. Weirdly enough, in react-router 5 I can do this (but only using the render prop), but then I can't use hooks in the function passed to the render prop.

So I'm curious what causes react to reconcile these changes vs when it does not cause it should see that the internal structure of each of these components is partially the same and only make the changes that are absolutely necessary... or so I thought.

Maybe the fact that the render prop function in react-router is not actually a component but just "a function that returns some react elements" (since you cannot use hooks) is why it works? Unsure.

Maybe the fact that the render prop function in react-router is not actually a component but just "a function that returns some react elements" (since you cannot use hooks) is why it works? Unsure.

That’s exactly why. Elements can be compared, components can’t. These two components might have the same JSX output, but they also can have different side-effects or state, so React will have to re-mount it.

I see, alright - so the only way, feasibly, to make this work is to use the render function pattern in wouter? Which means that function would also not support functions since it's a regular function?

In any case I'll go ahead and close this issue since it is expected behaviour - thanks for your help!

Just verified, it indeed works fine with a child render function with the same caveat of not being able to use hooks in that function.