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"
},
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.