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:
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:
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:
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.
Nick Scialli is a senior UI engineer at Microsoft.