Search code examples
javascriptreactjsreact-router-dom

Why does useState get shared across routes with different props?


I have an app that has two tabs "Apple" and "Banana". Each tab has a counter that is implemented with useState.

const Tab = ({ name, children = [] }) => {
  const id = uuid();
  const [ count, setCount ] = useState(0);

  const onClick = e => {
    e.preventDefault();
    setCount(c => c + 1);
  };

  const style = {
    background: "cyan",
    margin: "1em",
  };

  return (
    <section style={style}>
      <h2>{name} Tab</h2>
      <p>Render ID: {id}</p>
      <p>Counter: {count}</p>
      <button onClick={onClick}>+1</button>
      {children}
    </section>
  );
};

What is confusing is that the counter state is shared between both tabs!

If I increment the counter on one tab and then switch to the other tab, the counter has changed there too.

Why is this?


Here is my complete app:

import React, { useState } from "react";
import { createRoot } from "react-dom/client";
import { v4 as uuid } from "uuid";
import { HashRouter as Router, Switch, Route, Link } from "react-router-dom";

const Tab = ({ name, children = [] }) => {
  const id = uuid();
  const [ count, setCount ] = useState(0);

  const onClick = e => {
    e.preventDefault();
    setCount(c => c + 1);
  };

  const style = {
    background: "cyan",
    margin: "1em",
  };

  return (
    <section style={style}>
      <h2>{name} Tab</h2>
      <p>Render ID: {id}</p>
      <p>Counter: {count}</p>
      <button onClick={onClick}>+1</button>
      {children}
    </section>
  );
};

const App = () => {
  const id = uuid();

  return (
    <Router>
      <h1>Hello world</h1>
      <p>Render ID: {id}</p>
      <ul>
        <li>
          <Link to="/apple">Apple</Link>
        </li>
        <li>
          <Link to="/banana">Banana</Link>
        </li>
      </ul>
      <Switch>
        <Route
          path="/apple"
          exact={true}
          render={() => {
            return <Tab name="Apple" />;
          }}
        />
        <Route
          path="/banana"
          exact={true}
          render={() => {
            return <Tab name="Banana" />;
          }}
        />
      </Switch>
    </Router>
  );
};

const container = document.getElementById("root");
const root = createRoot(container);

root.render(<App />);

Versions:

  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router": "5.2.1",
    "react-router-dom": "5.2.1",
    "uuid": "^9.0.0"
  },

Solution

  • It has to do with the way Switch works in react-router-dom

    Ultimately, your tree of components remains identical, even when you switch routes.

    It's always a Router -> Switch -> Route -> Tab

    Because of the way Switch works, React never "mounts" a new component, it just reuses the old tree, because it can.

    I've run into this exact issue before, the fix is to add a key somewhere, like on the Tab or the Route. I usually add it to the Route because it makes more sense in my mind:

          <Switch>
            <Route
              key={'apple'}
              path="/apple"
              exact={true}
              render={() => {
                return <Tab name="Apple" />;
              }}
            />
            <Route
              key={'banana'}
              path="/banana"
              exact={true}
              render={() => {
                return <Tab name="Banana" />;
              }}
            />
          </Switch>
    

    Check this stackblitz:

    https://stackblitz.com/edit/react-gj5mcv?file=src/App.js

    Of course, your state gets reset in each tab when they get unmounted, which may or may not be desirable. But the solution to that, of course (if it's a problem for you), is, as usual, to lift state up.