React, how to dynamically choose a component to render
By Flavio Copes
Learn how to dynamically choose which React component to render, from React.createElement and an object lookup to just passing the component itself as a value.
To dynamically choose which React component to render, store the component itself in a variable (or in your data), assign it to a capitalized variable, and render that. A component is just a value, so you can pass it around like any other value.
Here’s the real case where I needed this, and the three solutions I went through.
I had the need to render a list of items in a menu, each with its own icon.
I initially hardcoded the component in a menu, like this:
const menu = [
{
title: 'Home',
icon: <HomeIcon className="mr-3 ml-1 h-5 w-5" />
},
{ title: 'Notifications', icon: <BellIcon className="mr-3 ml-1 h-5 w-5" /> },
{ title: 'Profile', icon: <UserIcon className="mr-3 ml-1 h-5 w-5" /> },
]
using the very cool @heroicons/react package.
In the JSX I iterated on the menu array to show {item.icon}.
But then I had to change the Tailwind classes I was using depending on the responsive state of the app, for which I used the react-responsive package. That meant I couldn’t bake the className into the data anymore. The data had to say which icon to use, and the rendering code had to decide how to render it.
First attempt: strings and React.createElement
I decided to pass a string instead, so I first did this:
const menu = [
{ title: 'Home', icon: 'HomeIcon' },
{ title: 'Notifications', icon: 'BellIcon' },
{ title: 'Profile', icon: 'UserIcon' },
]
const Icon = (props) => {
const { name } = props
let icon = null
if (name === 'HomeIcon') icon = HomeIcon
if (name === 'BellIcon') icon = BellIcon
if (name === 'UserIcon') icon = UserIcon
return React.createElement(icon, { ...props })
}
...
<Icon name={item.icon} />
Why not render the string directly? Because JSX doesn’t work that way. A lowercase tag like <div> becomes an HTML element, and a capitalized tag must reference a component in scope. A string variable is neither, so we have to map the string to the actual component ourselves.
Better: an object lookup
Another possible solution is to use an object to look up the components, instead of a bunch of if checks:
const icons = {
HomeIcon,
BellIcon,
UserIcon,
}
const Icon = (props) => {
const { name } = props
const TheIcon = icons[name]
return <TheIcon {...props} />
}
<Icon name={item.icon} />
I had to use
TheIconas React components start with an uppercase letter by convention.
Be careful with the lookup, though. If name contains a string that’s not in the icons object, TheIcon is undefined and React crashes with “Element type is invalid”. A typo in the data is enough to trigger it. If your strings come from anywhere you don’t fully control, check the lookup result before rendering.
The simplest way: pass the component itself
This was good enough, but I then realized I could do it in a much simpler way, by using the actual component instead of a string in the menu array:
const menu = [
{ title: 'Home', icon: HomeIcon },
{ title: 'Notifications', icon: BellIcon },
{ title: 'Profile', icon: UserIcon },
]
const Icon = (props) => {
const { icon } = props
const TheIcon = icon
return <TheIcon {...props} />
}
...
<Icon icon={item.icon} />
No string mapping, no lookup table to keep in sync. The component reference travels with the data, and the Icon wrapper decides the props (like the responsive Tailwind classes) at render time.