Search code examples
javascriptreactjseval

Rendering React Components from String


I'm trying to create multiple React components dynamically from a string entered by the user. Each letter is supposed to be rendered as a separate component.

My plan was to create an array from the string and map over it returning each component in a new array.

My main issue is to convert the letter (string) into the component's name.

import React from 'react'
import './App.css'

import A from './ComponentA'
import B from './ComponentB'
import C from './ComponentC'

const userInput = "ABC"
const userInputArray = userInput.split("")

const components = userInputArray.map((comp, index) => {
  return (
    React.createElement(eval(comp), { key: [index]})
  )
})

function App() {

  return (
    <>
    { components }
    </>
  )
}

export default App

I thought I found a solution using 'eval' but it fails when minified in production.

(Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: number.)


Solution

  • There is really no reason to use eval() at all: you can simply use a dictionary to store your component references, and then render it accordingly.

    Note that I have used i as a key, but you really should not use it but instead rely on a stable, unique identifier.

    import A from "./ComponentA";
    import B from "./ComponentB";
    import C from "./ComponentC";
    
    // NOTE: Added 'D' to test for component that doesn't exist
    const userInput = "ABCD";
    const userInputArray = userInput.split("");
    
    const components = { A, B, C };
    
    export default function App() {
      return userInputArray.map((component, i) => {
        const Component = components[component];
        if (!Component) return <></>;
    
        return <Component key={i} />;
      });
    }
    

    Edit Dynamic components

    However, what I would recommend is that you take advantage of React lazy loading and suspense feature, so that you don't have to load all components at once if the user input array does not need all of them. It is little more advanced, but here is another proof-of-concept example:

    • It allows you to input dynamic string that is parsed to load in the necessary components
    • It uses React.Suspense + React.lazy to perform dynamic loading
    import { lazy, Suspense, useState } from "react";
    
    const components = {
      A: lazy(() => import("./ComponentA")),
      B: lazy(() => import("./ComponentB")),
      C: lazy(() => import("./ComponentC"))
    };
    
    export default function App() {
      const [userInput, setUserInput] = useState("ABCD");
      return (
        <>
          <input
            type="text"
            value={userInput}
            onChange={(e) => setUserInput(e.currentTarget.value)}
          />
          <Suspense fallback="Loading...">
            {userInput.split("").map((component, i) => {
              const Component = components[component.toUpperCase()];
              if (!Component) return <></>;
    
              return <Component key={i} />;
            })}
          </Suspense>
        </>
      );
    }
    

    Edit Dynamic components with lazy loading