I have an application with two tabs "Apple" and "Banana". Each tab has a counter implemented using 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's confusing is that the counter state is shared between the two tabs!
If I increment the counter on one tab and then switch to another tab, the counter also changes.
why is that?
This is my complete application:
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 />);
Version:
"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" },
Adam has a good explanation and answer on what's going on here, it's an optimization that doesn't tear down and reinstall the same React component just because the URL path changes. Using React keys will definitely solve this problem, forcing React to remount the
Tab
component, thereby "resetting" thecount
state.I suggest another approach, when the
name
attribute changes from"apple"
to"banana"
, keep the routing component mounted and simple to reset thecount
status and vice versa.This will make RRD optimization work for you, not against you.
If you don't have a passed prop like
name
to get hints from, you can uselocation.pathname
. Note that this does couple some internal component logic with external details.Example:
This works with
Switch
in react-router-domUltimately, your component tree remains the same even if you switch routes.
Always Router->Switch->Routing->Tab
Due to the way Switch works, React never "installs" new components, it just reuses the old tree because it can.
I've had this problem before and the solution was to add a key somewhere, like on
Tab
orRoute
. I usually add this toRoute
because it makes more sense to me:Check out this stack blitz:
https://stackblitz.com/edit/react-gj5mcv ?file=src/App.js
Of course, your state will be reset in each tab when each tab is unloaded, which may or may not be ideal. But the solution to this is of course (if this is an issue for you), as usual, to boost status.