How to create a React component dynamically with JSON

By Hunter Becton on June 24, 2021

I was working on a project that required hundreds of components that would be used as templates for editing and exporting social media graphics. I could have created every component in the code base, but I was worried this would make the code unnecessarily bloated and hard to manage. So, I thought to myself, is it possible to render React components from JSON?

With a quick Google search, I found a helpful resource from Gaurav Singhal on Pluralsight: How to Render a Component Dynamically Based on a JSON Config. I took the general approach that Gaurav laid out in the tutorial and included Tailwind CSS for styling.

Watch the lesson

Project repo

The code for this project is open-source and available on GitHub. I use Gumroad for those that want to be generous and donate, but no purchase is required. 😊

Grab the code.

createElement()

createElement() is part of React and is responsible for rendering the UI. Most of the time when you're creating a React component you're writing JSX, but did you know the JSX is converted to use createElement()? In other words, you can use React without JSX, which is what makes rendering a React component from JSON possible.

Next.js and Tailwind CSS starter

As long as you're using React, feel free to use any additional frameworks or libraries you want when implementing something similar in your projects. I chose to use Next.js and Tailwind. You can read more in the Tailwind installation guide if you're interested in how to implement Tailwind into Next.js or another React framework like Create React App or Gatsby.

JSON configuration

How you configure your JSON is crucial because it acts as the blueprint for your dynamically rendered components. The good news is that the structure is quite simple if you think about it in the same way HTML is written.

To start, you have your first component, which will be a string value that will be mapped to a component in the renderer. Each component will have children, which are just more components nested in the HTML tree. This pattern continues to repeat itself for as long as it needs to in order for elements to be rendered in the correct order.

Any other values in the JSON, like id,className, style, and ariaHidden will be treated like regular props and passed into the components. Also, because I'm using Tailwind for my styling, the classes are standard Tailwind classes. If you decided to use your own classes you would have to ensure that the CSS file is imported into your application.

Below is an example of a configuration file for rendering a simple card component:

export const config = {
  component: 'li',
  id: 'cardWrapper',
  className: 'col-span-1 flex shadow-sm rounded-md',
  children: [
    {
      component: 'div',
      id: 'initialWrapper',
      className:
        'shrink-0 flex items-center justify-center w-16 text-white text-sm font-medium rounded-l-md',
      styles: [
        {
          name: 'backgroundColor',
          value: '#6366F1',
        },
      ],
      children: 'HB',
    },
    {
      component: 'div',
      id: 'infoWrapper',
      className:
        'flex-1 flex items-center justify-between border-t border-r border-b border-gray-lighter bg-white rounded-r-md truncate',
      children: [
        {
          component: 'div',
          id: 'info',
          className: 'flex-1 px-4 py-2 text-sm truncate',
          children: [
            {
              component: 'p',
              id: 'title',
              className: 'text-black font-medium hover:text-gray-base',
              children: 'GraphQL',
            },
            {
              component: 'p',
              id: 'readTime',
              className: 'text-gray-base',
              children: '10 min read',
            },
          ],
        },
        {
          component: 'div',
          id: 'buttonWrapper',
          className: 'shrink-0 pr-2',
          children: [
            {
              component: 'button',
              id: 'optionButton',
              className:
                'w-8 h-8 bg-white inline-flex items-center justify-center text-gray-mid rounded-full bg-transparent hover:text-gray-base focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500',
              children: [
                {
                  component: 'span',
                  id: 'optionButtonSr',
                  className: 'sr-only',
                  children: 'Open Options',
                },
                {
                  component: 'verticalDots',
                  id: 'verticalDots',
                  className: 'w-5 h-5',
                  ariaHidden: 'true',
                },
              ],
            },
          ],
        },
      ],
    },
  ],
}

Component renderer

With the configuration done we now need to write the renderer function, which will take the configuration and render the component with createElement().

Below is the code for the complete renderer:

import { createElement } from 'react'
import { Div } from './Div'
import { Li } from './Li'
import { P } from './P'
import { Button } from './Button'
import { VerticalDots } from './VerticalDots'

const keysToComponentMap = {
  div: Div,
  li: Li,
  p: P,
  button: Button,
  verticalDots: VerticalDots,
}

const stylesMap = styles => {
  let mappedStyles = {}
  styles.forEach(style => {
    mappedStyles[style.name] = style.value
  })
  return mappedStyles
}

export const renderComponent = config => {
  if (typeof keysToComponentMap[config.component] !== 'undefined') {
    return createElement(
      keysToComponentMap[config.component],
      {
        id: config.id,
        key: config.id,
        className: config.className ? config.className : null,
        ariaHidden: config.ariaHidden ? config.ariaHidden : null,
        style: config.styles ? stylesMap(config.styles) : null,
      },
      config.children &&
        (typeof config.children === 'string'
          ? config.children
          : config.children.map(c => renderComponent(c))),
    )
  }
}

keysToComponentMap

Part of the renderer will be responsible for mapping the string values in the component field to an actual React component. This is what the keysToComponentMap is responsible for:

const keysToComponentMap = {
  div: Div,
  li: Li,
  p: P,
  button: Button,
  verticalDots: VerticalDots,
}

The key will be the name of the string passed in to the component value in the JSON configuration. The value will be the actual component, which you'll need to create and import into the renderer.

The components in this example are simple and pass the props they get from the configuration. Below are a couple of examples of the components.

Div component

export const Div = ({ className, style, id, children }) => {
  return (
    <div id={id} className={className} style={style}>
      {children}
    </div>
  )
}

Button component

export const Button = ({ className, style, id, children }) => {
  return (
    <button id={id} className={className} style={style}>
      {children}
    </button>
  )
}

VerticalDots component

import { BiDotsVerticalRounded } from 'react-icons/bi'

export const VerticalDots = ({ className, style, id }) => {
  return <BiDotsVerticalRounded id={id} className={className} style={style} />
}

stylesMap

The stylesMap function is responsible for taking the array of styles in the configuration file and formating them appropriately for inline styles. Below is the code for this function:

const stylesMap = styles => {
  let mappedStyles = {}
  styles.forEach(style => {
    mappedStyles[style.name] = style.value
  })
  return mappedStyles
}

When you pass in the following array: [{name: 'backgroundColor', value: '#6366F1'}] into the function, the following will be returned: {backgroundColor: #6366F1}

renderComponent

The renderComponent is where createElement() is called. createElement() takes three arguments as described in the React Top-Level API documentation:

React.createElement(type, [props], [...children])

The type argument refers to what element or component you want to be rendered. The component string value will be used to reference the appropriate component that needs to be returned like so: keysToComponentMap[config.component].

The next argument is all the props that will be passed into the components. Not all the components will include the className, ariaHidden, or style props, so ternary is used to check if the value is passed into the configuration. If not, the value returned will be null.

The last argument is all the children in a component. It first checks to see if the value passed into children is a type string. If it is a string value then the string will be returned. Some common use cases for using a string as a child is for rendering text between anchor or paragraph tags.

If the type is not a string then the component is passed into the renderComponent() function. This type of pattern, where a function calls itself inside the function, is called recursion. This is needed in order to handle deeply nested children. In other words, the function will continue to work its way through the component configuration until there are no more nested children.

Tailwind safelist

When your application builds Tailwind will only ship the CSS classes it needs. Because these components are dynamically rendered your production build will not know about them, so you'll need to safelist the styles to ensure they'll be included in your production build. In order to safelist styles, you'll need to include the following in your tailwind.config.js file:

module.exports = {
  mode: 'jit',
  purge: {
    content: [
      './pages/**/*.{js,ts,jsx,tsx}',
      './components/**/*.{js,ts,jsx,tsx}',
    ],
    safelist: [
      'col-span-1',
      'flex',
      'shadow-sm',
      'rounded-md',
      'shrink-0',
      'items-center',
      'justify-center',
      'w-16',
      'text-white',
      'text-sm',
      'font-medium',
      'rounded-l-md',
      'flex-1',
      'px-4',
      'py-2',
      'pr-2',
      'w-8',
      'h-8',
      'inline-flex',
      'text-gray-mid',
      'rounded-full',
      'bg-transparent',
      'hover:text-gray-base',
      'focus:outline-none',
      'focus:ring-2',
      'focus:ring-offset-2',
      'focus:ring-indigo-500',
    ],
  },
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Using the renderer

In a production project, I would fetch the JSON configuration from a database and parse the JSON string. In this example, I just need to import the configuration file from the local project. There's no need to parse the JSON because it's set to an exported constant named config.

Once the config is imported you'll need to import the renderComponent. To use the renderer you can write the following code in /pages/index.js:

import { config } from '/config'
import { renderComponent } from '/components'

export default function Home() {
  return (
    <div className="flex min-h-screen flex-col p-4">
      <ul className="mt-3 grid grid-cols-1 gap-5 sm:grid-cols-2 sm:gap-6 lg:grid-cols-4">
        {renderComponent(config)}
      </ul>
    </div>
  )
}

Conclusion

Prior to this project I never used createElement() to render React components. I learned a lot about React in this process and was pleased to discover that rendering a component from JSON wasn't complicated. I hope what I shared today helps you in your next project!