Search code examples
reactjstypescriptfrontend

How to structure a reusable and modular dropdown menu (or any modular component really) in React


I recently developed a multi-level dropdown menu, and I had to think about making this component as reusable as possible.

My best take on this was to pass down to the component an object, defining the actual structure of the dropdown menu.

Something like this:

const content = [
  {
    value: "Item 1",
    onClick: () => console.log("Clicked 1"),
  },
  {
    value: "Item 2",
    children: [
      {
        value: "Item 2.1",
        children: [
          {
            value: "Item 2.1.1",
            onClick: () => mockFN,
          },
          {
            value: "Item 2.1.2",
            onClick: () => mockFN,
          },
          {
            value: "Item 2.1.3",
          },
          {
            value: "Item 2.1.4",
          },
          {
            value: "Item 2.1.5",
            onClick: () => mockFN,
          },
        ],
      },
    ],
  },
];

return (
    <Dropdown content={content} trigger={"Test"} />
);

Of course, this being in TypeScript, everything is handled with a type:

type DropdownItem = {
  value: string;
  color?: string;
  textColor?: string;
  children?: DropdownItem[];
  onClick?: () => void;
};

What I noticed browsing online though, is that some components like this one are actually handled in a different way, for example react-multilevel-dropdown actually exposes not only a Dropdown component, but also some sub-components like Dropdown.Item and Dropdown.Submenu

<Dropdown
  title='Dropdown title'
>
  <Dropdown.Item
    onClick={() => doSomething()}
  >
    Item 1
  </Dropdown.Item>
  <Dropdown.Item>
    Item 2
    <Dropdown.Submenu>
      <Dropdown.Item>
        Subitem 1
      </Dropdown.Item>
    </Dropdown.Submenu>
  </Dropdown.Item>
</Dropdown>

Now here come the questions:

  1. Is there a name to identify these 2 ways of structuring a component?

  2. Should I always prefer one over the other? Or does it depend on what's to be done? One thing that comes to my mind is, if the component's content needs to be generated by a backend payload, the "object" one would be significantly easier to deal with (that's why I went with that route in the end)

  3. How are the subcomponents coded like? I'm pretty sure I can't name a component with a period in it. So is it something like Dropdown is a class component, and Dropdown.Item is a property of that class?

Thanks for the help!


Solution

  • Ok I went a bit more in depth on the subject, and basically found that:

    1. I haven't found a name for the "object" one, but the other is referred to as Compound Component
    2. As I thought, there's no definitive answer to which one is better as it depends on the use case.
    3. In the Compound Components pattern, the subcomponents are usually defined as static properties of the main component, like this:
    import React from 'react';
    
    type DropdownProps = { title: string; children: React.ReactNode };
    type ItemProps = { onClick?: () => void; children: React.ReactNode };
    
    const Dropdown: React.FC<DropdownProps> & { Item: React.FC<ItemProps> } = (props) => {
      return <div>{props.children}</div>;
    };
    
    Dropdown.Item = (props) => {
      return <div onClick={props.onClick}>{props.children}</div>;
    };
    
    export default Dropdown;