I am currently working with React components, and I’m experiencing an issue with shared state in the Area component
. The component has a count state
, and the problem arises when multiple instances of the component are created.
function Area({ title }) {
const [count, setCount] = useState(0);
return (
<div>
<h2>current area : {title}</h2>
<div>
<button type="button" onClick={() => setCount(count + 1)}>
+1
</button>
<div>count : {count}</div>
</div>
</div>
);
}
const menuInfos = [
{ title: "aaaaaa", children: <Area title="aaaaaa" /> },
{ title: "bbbbbb", children: <Area title="bbbbbb" /> },
{ title: "cccccc", children: <Area title="cccccc" /> },
{ title: "dddddd", children: <Area title="dddddd" /> },
{ title: "eeeeee", children: <Area title="eeeeee" /> }
];
In the current code, the Area component
is used within the Menu component
, and an instance is created for each menu item. However, when Area component
is used across multiple menu items, the count variable is shared, leading to unexpected behavior.
Code sandbox working link : https://codesandbox.io/s/goofy-wind-59v5j9?file=/src/_app.js
I would like to understand reasons about this work and to find a solution to manage the count state eparately for each menu item. I would appreciate any advice on how to modify the code to achieve this.
I have utilized the useEffect hook
within the Area component
to log the unmount phase using `console.log, and the unmount process appears to be functioning correctly.
Additionally, I compared the children components from the menuInfos array
, which are passed as props
to the Menu component
, using Object.is, and they are recognized as distinct objects.
However, despite these observations, I am struggling to fully comprehend the issue at hand. It seems that I might have overlooked something crucial in my understanding.
6
Answers
I will be fully honest with you: I can not completely explain, why your solution is not working. My best guess is, that your "children" are not really separate instances of
Area
, but instead the same instance with just different titles.I rewrote your element mapping, so that it is working as intended. Please try it:
As you can see I duplicated the mapping of your menuInfos to create truly separate instances of
Area
with a conditional statement to only show the correctArea
. The count-States are now separated from each other.React uses a property called
key
during rendering so it does not update things that do not need updating. If a parent is rendered that has a child for whichkey
is the same as before, then it will do an optimized, reduced kind of rendering which apparently means that updates for{count}
are not being done. It’s a different component, but React does not know it.Adding
key
properties will make your code work:Update: the code "works" but maybe not as you want… Because you only render one component at a time, the others seem to get unloaded and lose their state. The next time a component is shown again, its state is created again and starts at 0 again.
Instead of setting your children components in an array you should use the array to store data that will populate your components. With a little CSS trick you can also keep your data even if you cicle between components:
Also, here is a working codesandbox example
Your code works same as below codes.
When
current
state changes,<Area />
going to be rendered with newtitle
prop.count
in<Area />
is initialized when the component first mounts (with 0), and there is no reason to render with the newcount
when its prop (title
) changes. So it is natural to persist whencurrent
changes.One of the working solutions is adding
useEffect
as below.In this way, you can sync the changes of
title
withcount
.The solution is to ensure all child elements have a different position.
This change will make your code work:
Why? Because an array is returned, e.g.
[false, false, <Area />]
so the visible Area element is in third position. React uses type and position to determine which element uses which state.But your Area elements will get unmounted when you switch between them, resetting their state every time.
You could counter that by having them all in the DOM and hiding them visually:
The issue you’re experiencing with the shared state in the
Area
component is because theuseState
hook creates local state that is specific to each instance of the component. In your current code, themenuInfos
array creates multiple instances of theArea
component, but they all share the samecount
state. You need to passkey
to each component ofArea
to identify it uniquely.Your updated code:
Additionally, if you also want to preserve the states for each component of Area you need to update your code like this:
State is isolated between components. React keeps track of which state belongs to which component based on their place in the UI tree. You can control when to preserve state and when to reset it between re-renders.
The UI tree
Browsers use many tree structures to model UI. The DOM represents HTML elements, the CSSOM does the same for CSS. There’s even an Accessibility tree!
React also uses tree structures to manage and model the UI you make. React makes UI trees from your JSX. Then React DOM updates the browser DOM elements to match that UI tree. In this case the UI tree will be able to preserve the state of each
Area
component as well because they are at unique position in the UI tree.Useful Resource: https://dev.to/muneebkhan4/how-react-preserve-and-reset-state-38la