TypeOfNaN

How to toggle and array of items separately in React

Nick Scialli
November 26, 2021

The core React concept of state management becomes a bit trickier when dealing with arrays.

Lets say we want to create a collapsible menu of all our pets. We might want the following effect:

collapsible pet menu

Note that each category of pet (Dog, Cat, Fish) is its own toggle to show the names of our animals within that category. They operate independently of each other, so any number of categories can be expanded or collapsed at the same time.

Defining our data and laying out the basic component

First let’s come up with a data structure that fits this use case. I’ll use an array of objects. Each of those objects will have a category key and an items key. The category will be a string—the type of animal. The items key will be an array of strings—the names of the animals in that category.

const menuItems = [
  {
    category: 'Dogs',
    items: ['Daffodil', 'Jackie', 'Spike'],
  },
  {
    category: 'Cats',
    items: ['Whiskers', 'Phillipe'],
  },
  {
    category: 'Fish',
    items: ['Sparky'],
  },
];

Next, we’ll set up a “dumb” component (i.e., with everything expanded and no toggling logic).

function App() {
  return (
    <div className="App">
      <h1>My Pets</h1>
      <ul>
        {menuItems.map((menu) => {
          return (
            <li key={menu.category}>
              <button>{menu.category} -</button>
              <ul>
                {menu.items.map((item) => {
                  return <li key={item}>{item}</li>;
                })}
              </ul>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

And this works great for a fully-expanded view:

expanded view

Our first attempt: a boolean state variable

We might think that expanded or collapsed is binary, so let’s use a boolean (true/false) state variable called isOpen. Using the button’s onClick handler, we’ll simply set isOpen to the opposite of its previous state. When isOpen is true, we show the sublist, if false, we hide the sublist. Let’s try it out!

function App() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="App">
      <h1>My Pets</h1>
      <ul>
        {menuItems.map((menu) => {
          return (
            <li key={menu.category}>
              <button
                onClick={() => {
                  setIsOpen(!isOpen);
                }}
              >
                {menu.category} {isOpen ? '-' : '+'}
              </button>
              {isOpen && (
                <ul>
                  {menu.items.map((item) => {
                    return <li key={item}>{item}</li>;
                  })}
                </ul>
              )}
            </li>
          );
        })}
      </ul>
    </div>
  );
}

Now we try it out in the browser:

toggling all items

Whoops! In hindsight, we should have seen this coming: we’re trying to maintain independent states for each category. If we have three categories, there’s simply no way we could maintain a boolean state for each category if we hve only one boolean.

A winning approach: maintaining many boolean states in an object

One structure that will work great for any number of categories is an object. Consider if we had an object in state that looked like this:

{
  Dogs: true,
  Cats: false,
  Fish: true,
}

With this object, we now have all the information we need! The Dogs menu should be open, Cats menu closed, and Fish menu open. We can get the right boolean value by accessing the appropriate property of the state object. For example, isOpen[category].

We could try creating an initial state that contains all our categories already, but it’s just as easy to start with an empty object. After all, we don’t need isOpen[category] to start out as false, it just needs to be falsy.

function App() {
  const [isOpen, setIsOpen] = useState({});

  const toggleOpen = (category) => {
    setIsOpen({
      ...isOpen,
      [category]: !isOpen[category],
    });
  };

  return (
    <div className="App">
      <h1>My Pets</h1>
      <ul>
        {menuItems.map((menu) => {
          return (
            <li key={menu.category}>
              <button
                onClick={() => {
                  toggleOpen(menu.category);
                }}
              >
                {menu.category} {isOpen[menu.category] ? '-' : '+'}
              </button>
              {isOpen[menu.category] && (
                <ul>
                  {menu.items.map((item) => {
                    return <li key={item}>{item}</li>;
                  })}
                </ul>
              )}
            </li>
          );
        })}
      </ul>
    </div>
  );
}

And now it works!

Conclusion

Remember that if you need multiple pieces of information, you need to be able to store that information somewhere! A primitive simply won’t give you that ability.

Note that an object is just one way to solve tis particular problem! Some might think a Set is more idiomatic, but the truth is any way to maintain an accounting of all the open categories will do.

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli