skip to Main Content

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.

share state working

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


  1. 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:

    export default function Menu({ menuInfos }) {
      const [current, setCurrent] = useState(menuInfos[0].title);
      return (
        <div>
          <div>
            <div>
              {menuInfos.map((info) => (
                <button
                  style={{ background: info.title === current ? "red" : "" }}
                  onClick={() => setCurrent(info.title)}
                >
                  {info.title}
                </button>
              ))}
            </div>
          </div>
          {menuInfos.map(
            (info, index) =>
              info.title === current && (
                <div key={index}>
                  <Area title={info.title} />
                </div>
              )
          )}
        </div>
      );
    }
    

    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 correct Area. The count-States are now separated from each other.

    Login or Signup to reply.
  2. 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 which key 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:

    const menuInfos = [
      { title: "aaaaaa", children: <Area key="a" title="aaaaaa" /> },
      { title: "bbbbbb", children: <Area key="b" title="bbbbbb" /> },
      { title: "cccccc", children: <Area key="c" title="cccccc" /> },
      { title: "dddddd", children: <Area key="d" title="dddddd" /> },
      { title: "eeeeee", children: <Area key="e" title="eeeeee" /> }
    ];
    

    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.

    Login or Signup to reply.
  3. 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:

    import { useState } from "react";
    
    // no need to set your children components here
    const menuInfos = [
      { title: "aaaaaa" },
      { title: "bbbbbb" },
      { title: "cccccc" },
      { title: "dddddd" },
      { title: "eeeeee" }
    ];
    
    function Area({ title }) {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <h2>current area : {title}</h2>
          <div>
            <button
              type="button"
              onClick={() => setCount((current) => current + 1)}
            >
              +1
            </button>
            <div>count : {count}</div>
          </div>
        </div>
      );
    }
    
    function Menu({ menuInfos }) {
      const [current, setCurrent] = useState(menuInfos[0].title);
    
      return (
        <div>
          <div>
            <div>
              {menuInfos.map((info) => (
                <button
                  key={info.title}
                  style={{
                    background: info.title === current ? "red" : ""
                  }}
                  onClick={() => setCurrent(info.title)}
                >
                  {info.title}
                </button>
              ))}
            </div>
          </div>
    
          {/** render here your components */}
          {menuInfos.map((info, index) => (
            <div
              key={index}
              {/** here's the trick to keep your data between changes of menus */}
              style={{
                display: info.title === current ? "block" : "none"
              }}
            >
              <Area title={info.title} />
            </div>
          ))}
        </div>
      );
    }
    
    export default function Index() {
      return (
        <div>
          <Menu menuInfos={menuInfos} />
        </div>
      );
    }
    
    

    Also, here is a working codesandbox example

    Login or Signup to reply.
  4. Your code works same as below codes.

    export default function Menu({ menuInfos }) {
      const [current, setCurrent] = useState(menuInfos[0].title);
      return (
        <div>
          <div>
            <div>
              {menuInfos.map((info) => (
                <button
                  style={{ background: info.title === current ? "red" : "" }}
                  onClick={() => setCurrent(info.title)}
                >
                  {info.title}
                </button>
              ))}
            </div>
          </div>
          <div>
            <Area title={current} />
          </div>
        </div>
      );
    }
    
    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>
      );
    }
    

    When current state changes, <Area /> going to be rendered with new title prop.

    count in <Area /> is initialized when the component first mounts (with 0), and there is no reason to render with the new count when its prop (title) changes. So it is natural to persist when current changes.

    One of the working solutions is adding useEffect as below.

    
    function Area({ title }) {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        setCount(0);
      }, [title])
      
      return (
        <div>
          <h2>current area : {title}</h2>
          <div>
            <button type="button" onClick={() => setCount(count + 1)}>
              +1
            </button>
            <div>count : {count}</div>
          </div>
        </div>
      );
    }
    

    In this way, you can sync the changes of title with count.

    Login or Signup to reply.
  5. The solution is to ensure all child elements have a different position.

    This change will make your code work:

    <div>
       {menuInfos.map((info) => info.title === current && info.children)}
    </div>
    

    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:

    <div>
      {menuInfos.map((info) => (
         <div style={info.title === current ? {} : { display: "none" }}>
            {info.children}
         </div>
       ))}
    </div>
    
    Login or Signup to reply.
  6. The issue you’re experiencing with the shared state in the Area component is because the useState hook creates local state that is specific to each instance of the component. In your current code, the menuInfos array creates multiple instances of the Area component, but they all share the same count state. You need to pass key to each component of Area to identify it uniquely.

    Your updated code:

    import { useState } from "react";
    
    export default function Menu({ menuInfos }) {
      const [current, setCurrent] = useState(menuInfos[0].title);
      return (
        <div>
          <div>
            <div>
              {menuInfos.map((info) => (
                <button
                  style={{ background: info.title === current ? "red" : "" }}
                  onClick={() => setCurrent(info.title)}
                >
                  {info.title}
                </button>
              ))}
            </div>
          </div>
          <div>{menuInfos.find((info) => info.title === current).children}</div>
        </div>
      );
    }
    
    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 key='a' title="aaaaaa" /> },
      { title: "bbbbbb", children: <Area key='b' title="bbbbbb" /> },
      { title: "cccccc", children: <Area key='c' title="cccccc" /> },
      { title: "dddddd", children: <Area key='d' title="dddddd" /> },
      { title: "eeeeee", children: <Area key='e' title="eeeeee" /> }
    ];
    
    export function App() {
      return (
        <div>
          <Menu menuInfos={menuInfos} />
        </div>
      );
    }
    

    Additionally, if you also want to preserve the states for each component of Area you need to update your code like this:

    import { useState } from "react";
    
    export default function Menu({ menuInfos }) {
      const [current, setCurrent] = useState(menuInfos[0].title);
      return (
        <div>
          <div>
            <div>
              {menuInfos.map((info) => (
                <button
                  style={{ background: info.title === current ? "red" : "" }}
                  onClick={() => setCurrent(info.title)}
                >
                  {info.title}
                </button>
              ))}
            </div>
          </div>
          <div>
            {menuInfos.map((info) => (
              <Area
                key={info.title}
                title={info.title}
              />
            ))}
          </div>
        </div>
      );
    }
    
    function Area({ title, chosen }) {
      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>
      );
    }
    
    export function App() {
      const menuInfos = [
        { title: "aaaaaa" },
        { title: "bbbbbb" },
        { title: "cccccc" },
        { title: "dddddd" },
        { title: "eeeeee" }
      ];
    
      return (
        <div>
          <Menu menuInfos={menuInfos} />
        </div>
      );
    }
    

    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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search