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"
},
2
Answers
It has to do with the way
Switch
works in react-router-domUltimately, 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 theRoute
. I usually add it to theRoute
because it makes more sense in my mind: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.
Adam has a great explanation and answer for what is happening here and that it is an optimization to not tear down and remount the same React component simply because the URL path changed. Using a React key will certainly solve this by forcing React to remount the
Tab
component and thus "reset" thecount
state.I’d suggest another method that keeps the routed component mounted and simply resets the
count
state when thename
prop changes from"apple"
to"banana"
and vise-versa.This will keep the RRD optimization working for you instead of against you.
If you didn’t have a passed prop like
name
to take a cue from, thenlocation.pathname
could be used. Note that this does couple some internal component logic to external details.Example: