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" },
P粉4968866462024-03-31 18:52:50
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" the count
state.
I suggest another approach, when the name
attribute changes from "apple"
to "banana"
, keep the routing component mounted and simple to reset the count
status and vice versa.
const Tab = ({ name, children = [] }) => { const id = uuid(); const [count, setCount] = useState(0); useEffect(() => { setCount(0); // <-- reset to 0 when name prop updates }, [name, setCount]); const onClick = e => { e.preventDefault(); setCount(c => c + 1); }; const style = { background: "cyan", margin: "1em", }; return (); }; {name} Tab
Render ID: {id}
Counter: {count}
{children}
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 use location.pathname
. Note that this does couple some internal component logic with external details.
Example:
const { pathname } = useLocation();
const [count, setCount] = useState(0);
useEffect(() => {
setCount(0);
}, [pathname, setCount]);
P粉6086470332024-03-31 18:42:56
This works with Switch
in react-router-dom
Ultimately, 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
or Route
. I usually add this to Route
because it makes more sense to me:
{ return ; }} /> { return ; }} />
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.