Create an instance of a React class from a string

28,849

Solution 1

This will not work:

var Home = React.createClass({ ... });

var Component = "Home";
React.render(<Component />, ...);

However, this will:

var Home = React.createClass({ ... });

var Component = Home;
React.render(<Component />, ...);

So you simply need to find a way to map between the string "Home" and the component class Home. A simple object will work as a basic registry, and you can build from there if you need more features.

var components = {
  "Home": Home,
  "Other": OtherComponent
};

var Component = components[this.props.template];

Solution 2

No need to manually map your classes to a dictionary, or "registry", as in Michelle's answer. A wildcard import statement is already a dictionary!

   import * as widgets from 'widgets';
   const Type = widgets[this.props.template];
   ...
   <Type />

You can make it work with multiple modules by merging all the dictionaries into one:

import * as widgets from 'widgets';
import * as widgets2 from 'widgets2';

const registry = Object.assign({}, widgets, widgets2);
const widget = registry[this.props.template];

I would totally do this to get dynamic dispatch of react components. In fact I think I am in a bunch of projects.

Solution 3

I had the same problem, and found out the solution by myself. I don't know if is the "best pratice" but it works and I'm using it currently in my solution.

You can simply make use of the "evil" eval function to dynamically create an instance of a react component. Something like:

function createComponent(componentName, props, children){
  var component = React.createElement(eval(componentName), props, children);
  return component;
}

Then, just call it where you want:

var homeComponent = createComponent('Home', [props], [...children]);

If it fits your needs, maybe you can consider something like this.

Hope it helps.

Solution 4

Here is the way it will work from a string content without embedding your components as statically linked code into your package, as others have suggested.

import React from 'react';
import { Button } from 'semantic-ui-react';
import createReactClass from 'create-react-class';

export default class Demo extends React.Component {
    render() {
        const s = "return { render() { return rce('div', null, rce(components['Button'], {content: this.props.propA}), rce(components['Button'], {content: 'hardcoded content'})); } }"
        const createComponentSpec = new Function("rce", "components", s);
        const componentSpec = createComponentSpec(React.createElement, { "Button": Button });
        const component = React.createElement(createReactClass(componentSpec), { propA: "content from property" }, null);

        return (
            <div>
                {component}
            </div>
        )
    }
}

The React class specification is in string s. Note the following:

rce stands for React.createElement and given as a first param when callingcreateComponentSpec.

components is a dictionary of extra component types and given as a second param when callingcreateComponentSpec. This is done so that you can provide components with clashing names.

For example string Button can be resolved to standard HTML button, or button from Semantic UI.

You can easily generate content for s by using https://babeljs.io as described in https://reactjs.org/docs/react-without-jsx.html. Essentially, the string can't contain JSX stuff, and has to be plain JavaScript. That's what BabelJS is doing by translating JSX into JavaScript.

All you need to do is replace React.createElement with rce, and resolve external components via components dictionary (if you don't use external components, that you can skip the dictionary stuff).

Here is equivalent what in the code above. The same <div> with two Semantic UI Buttons in it.

JSX render() code:

function render() {
  return (
    <div>
      <Button content={this.props.propA}/>
      <Button content='hardcoded content'/>
    </div>
  );
}

BabelJS translates it into:

function render() {
  return React.createElement("div", null, React.createElement(Button, {
    content: this.props.propA
  }), React.createElement(Button, {
    content: "hardcoded content"
  }));
}

And you do replacement as outlined above:

render() { return rce('div', null, rce(components['Button'], {content: this.props.propA}), rce(components['Button'], {content: 'hardcoded content'})); }

Calling createComponentSpec function will create a spec for React class.

Which then converted into actual React class with createReactClass.

And then brought to life with React.createElement.

All you need to do is return it from main component render func.

Solution 5

I wanted to know how to create React classes dynamically from a JSON spec loaded from a database and so I did some experimenting and figured it out. My basic idea was that I wanted to define a React app through a GUI instead of typing in code in a text editor.

This is compatible with React 16.3.2. Note React.createClass has been moved into its own module.

Here's condensed version of the essential parts:

import React from 'react'
import ReactDOMServer from 'react-dom/server'
import createReactClass from 'create-react-class'

const spec = {
  // getDefaultProps
  // getInitialState
  // propTypes: { ... }
  render () {
    return React.createElement('div', null, 'Some text to render')
  }
}
const component = createReactClass(spec)
const factory = React.createFactory(component)
const instance = factory({ /* props */ })
const str = ReactDOMServer.renderToStaticMarkup(instance)
console.log(str)

You can see a more complete example here:

https://github.com/brennancheung/02-dynamic-react/blob/master/src/commands/tests/createClass.test.js

Share:
28,849
ewan
Author by

ewan

Updated on January 01, 2022

Comments

  • ewan
    ewan over 2 years

    I have a string which contains a name of the Class (this is coming from a json file). This string tells my Template Class which layout / template to use for the data (also in json). The issue is my layout is not displaying.

    Home.jsx:

    //a template or layout.
    var Home = React.createClass({
      render () {
        return (
        <div>Home layout</div>
        )
      }
    });
    

    Template.jsx:

    var Template = React.createClass({
      render: function() {
        var Tag = this.props.template; //this is the name of the class eg. 'Home'
        return (
            <Tag />
        );
      }
    });
    

    I don't get any errors but I also don't see the layout / Home Class. I've checked the props.template and this logs the correct info. Also, I can see the home element in the DOM. However it looks like this:

    <div id='template-holder>
        <home></home>
    </div>
    

    If I change following line to:

    var Tag = Home;
    //this works but it's not dynamic!
    

    Any ideas, how I can fix this? I'm sure it's either simple fix or I'm doing something stupid. Help would be appreciated. Apologies if this has already been asked (I couldn't find it).

    Thanks, Ewan

  • ewan
    ewan about 9 years
    Thanks, I was hoping to avoid having maps / links in the code. I would like to be able to add templates without changing the main code base. Is this not possible?
  • Michelle Tilley
    Michelle Tilley about 9 years
    @ewan You could build some kind of global registry and register each component when it gets created with React.createClass (e.g. by wrapping it in a function call or something), but you will definitely need to get references to the actual components.
  • AlexGrafe
    AlexGrafe over 6 years
    I wouldn't consider eval to be evil unless a user can enter anything they want.
  • tettoffensive
    tettoffensive about 4 years
    Awesome @MichelleTilley! my linter even simplified to var components = { Home, Other, }; and it worked fine.