Using named destructuring arguments to clean up your JSX

Have you ever found yourself creating JSX elements that are similar to each other but with slight differences? Drying up the code could take a lot of work in this situation. This week, I ran into this, and the solution was pretty elegant.

The problem

I was working on a project where I needed to create a series of buttons with different actions and icons. The buttons were all very similar but with slight differences. Here's an example of what the buttons looked like:

import { MenuItem } from '@headlessui/react'
import { UnlockIcon, LockIcon } from '@heroicons/react/20/solid'
import { lock, unlock } from './actions'

export const Menu = () => {
  return (
    <MenuItem onClick={lock}>
      <LockIcon />
      Lock
    </MenuItem>
    <MenuItem onClick={unlock}>
      <UnlockIcon />
      Unlock
    </MenuItem>
  )
}

This might not look like too much duplication, but remember that each of those menu items had a lot of tailwindcss classes that were identical between them. I also actually had five of these, not two. This was a perfect opportunity to dry up the code. The challenge was those icons. Each button uses a different one. At first I tried something like this:

const menuOptions = [
  { action: lock, icon: LockIcon, text: 'Lock' },
  { action: unlock, icon: UnlockIcon, text: 'Unlock' },
]

const MenuItemButton = ({ action, icon, text }) => {
  return (
    <MenuItem onClick={action}>
      <icon />
      {text}
    </MenuItem>
  )
}

This didn't work because JSX couldn't handle the icon prop the way I wanted it to. There isn't any HTML element called icon (although there is an i), so it didn't know what to do with it. I needed to find a way to pass the icon as a prop and render it as a JSX element.

The solution

In my case, the solution was to use a named destructuring argument. Here's what the final code looked like:

import { MenuItem } from '@headlessui/react'
import { UnlockIcon, LockIcon } from '@heroicons/react/20/solid'
import { lock, unlock } from './actions'

const menuOptions = [
  { action: lock, icon: LockIcon, text: 'Lock' },
  { action: unlock, icon: UnlockIcon, text: 'Unlock' },
]

export const Menu = () => {
  return (
    <div>
      {menuOptions.map((option) => (
        <MenuItemButton {...option} />
      ))}
    </div>
  )
}

const MenuItemButton = ({ action, icon: Icon, text }) => {
  return (
    <MenuItem onClick={action}>
      <Icon />
      {text}
    </MenuItem>
  )
}

I could have also named the menu option "Icon" instead of "icon" in the options array, but I wanted to keep my prop names consistent. This solution allowed me to name the prop icon while rendering a JSX element. This was a great way to dry up my code, and I hope it helps you, too!