Home > Mobile >  Toggle one list item at a time with React/TS functional components
Toggle one list item at a time with React/TS functional components

Time:05-17

I have a simple component with a few nested bits of mark-up:

    import React, { ReactElement } from "react";

    type MenuItem = {
        title: string;
        subItems?: Array<string>;
    };

    type MenuConfig = Array<MenuItem>;

    function Item({ item }: { item: MenuItem }): ReactElement {
        const [showItem, setShowItem] = React.useState(false);
        const handleShowItem = (): void => {
            setShowItem(!showItem);
        };
        return (
            <>
                {item.subItems && (
                    <button onClick={() => handleShowItem()}>
                        {showItem ? "Hide" : "Expand"}
                    </button>
                )}
                {showItem && <SubItem item={item} />}
            </>
        );
    }

    function SubItem({ item }: { item: MenuItem }): ReactElement {
        const { title } = item;
        return (
            <ul>
                {item?.subItems?.map((subitem: string, i: number) => (
                    <li key={i}>
                        {subitem}
                    </li>
                ))}
            </ul>
        );
    }

    function Solution({ menuConfig }: { menuConfig: MenuConfig }): ReactElement {
        return (
            <>
                {menuConfig.map((item: MenuItem, i: number) => (
                    <div key={i}>
                        <span>{item.title}</span>
                        <Item item={item} />
                    </div>
                ))}
            </>
        );
    }

    export default Solution;

This is what I am passing in:

menuConfig={[
                {
                    title: "About",
                },
                {
                    title: "Prices",
                    subItems: ["Hosting", "Services"],
                },
                {
                    title: "Contact",
                    subItems: ["Email", "Mobile"],
                },
            ]}

Now, it functions as expected, if an item contains subItems then an Expand button will be show which, if clicked, will only expand the relevant list.

How should I go about making sure only one list would be open at a time, given my implementation?

So if the user clicks Expand on a button, the other previously expanded lists should close.

I can't mess with the data that's coming in, so can't add ids to the object, but titles are unique.

I've searched and whilst there are a few examples, they don't help me, I just can't wrap my head around this.

CodePudding user response:

This is a perfect use case for React context. You essentially want a shared state among your menu. You can achieve this with prop drilling however contexts are a much cleaner solution.

Code sandbox

const MenuContext = createContext<{
  expandedState?: [number, React.Dispatch<React.SetStateAction<number>>];
}>({});
function Solution(): ReactElement {
  const expandedState = useState<number>(null);
  const value = { expandedState };
  return (
    <>
      <MenuContext.Provider value={value}>
        {menuConfig.map((item: MenuItem, i: number) => (
          <div key={i}>
            <span>{item.title}</span>
            <Item i={i} item={item} />
          </div>
        ))}
      </MenuContext.Provider>
    </>
  );
}
function Item({ item, i }: { item: MenuItem; i: number }): ReactElement {
  const [expanded, setExpanded] = useContext(MenuContext)?.expandedState ?? [];
  const shown = expanded === i;
  return (
    <>
      {item.subItems && (
        <button onClick={() => setExpanded?.(i)}>
          {shown ? "Hide" : "Expand"}
        </button>
      )}
      {shown && <SubItem item={item} />}
    </>
  );
}
  • Related