317

I am trying to dynamically render components based on their type.

For example:

var type = "Example";
var ComponentName = type + "Component";
return <ComponentName />; 
// Returns <examplecomponent />  instead of <ExampleComponent />

I tried the solution proposed here React/JSX dynamic component names

That gave me an error when compiling (using browserify for gulp). It expected XML where I was using an array syntax.

I could solve this by creating a method for every component:

newExampleComponent() {
    return <ExampleComponent />;
}

newComponent(type) {
    return this["new" + type + "Component"]();
}

But that would mean a new method for every component I create. There must be a more elegant solution to this problem.

I am very open to suggestions.

EDIT: As pointed out by gmfvpereira these days there is an official documentation entry for this: https://reactjs.org/docs/jsx-in-depth.html#choosing-the-type-at-runtime

Sam
  • 4,205
  • 2
  • 17
  • 13

16 Answers16

281

<MyComponent /> compiles to React.createElement(MyComponent, {}), which expects a string (HTML tag) or a function (ReactClass) as first parameter.

You could just store your component class in a variable with a name that starts with an uppercase letter. See HTML tags vs React Components.

var MyComponent = Components[type + "Component"];
return <MyComponent />;

compiles to

var MyComponent = Components[type + "Component"];
return React.createElement(MyComponent, {});
Alexandre Kirszenberg
  • 35,938
  • 10
  • 88
  • 72
  • 8
    Future readers will also likely find `{...this.props}` useful to transparently pass props to subtyped components from parent. Like `return ` – Dr.Strangelove May 20 '16 at 02:47
  • 7
    Also make sure you capitalize your dynamic variable name. – saada Oct 05 '16 at 22:14
  • 51
    Keep in mind that your **variable should hold the component** itself and **not** just the name of the component as **a string**. – totymedli Jan 17 '17 at 11:22
  • 4
    If you're also wondering **why** the var needs to be capitalised https://facebook.github.io/react/docs/jsx-in-depth.html#user-defined-components-must-be-capitalized – Aurelio Aug 15 '17 at 11:51
  • 8
    Components is undefined – ness-EE Feb 12 '18 at 11:55
  • 2
    for people confused on "Components is undefined" read "Choosing the Type at Runtime" section here https://reactjs.org/docs/jsx-in-depth.html#html-tags-vs.-react-components – Flemin Adambukulam Oct 11 '18 at 07:54
250

There is an official documentation about how to handle such situations is available here: https://facebook.github.io/react/docs/jsx-in-depth.html#choosing-the-type-at-runtime

Basically it says:

Wrong:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
    photo: PhotoStory,
    video: VideoStory
};

function Story(props) {
    // Wrong! JSX type can't be an expression.
    return <components[props.storyType] story={props.story} />;
}

Correct:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
    photo: PhotoStory,
    video: VideoStory
};

function Story(props) {
    // Correct! JSX type can be a capitalized variable.
    const SpecificStory = components[props.storyType];
    return <SpecificStory story={props.story} />;
}
gmfvpereira
  • 2,704
  • 1
  • 14
  • 5
  • 72
    VERY IMPORTANT THING: a **capitalized** variable – mpyw Jun 30 '17 at 04:57
  • 5
    Thanks for great answer. For the following readers, note that the value within the map object(the map object here is const components and the value is PhotoStory and VideoStory) must be *without* quotation marks - Otherwise, the component will not render and you will get error - in my case I missed it and just wasted time ... – Erez Lieberman Jul 15 '19 at 11:19
  • This should really be the accepted answer. It is way more clear with a better example. – Patrick Yan Oct 28 '22 at 23:21
  • For me it gives the following warning when using this: `Warning: Functions are not valid as a React child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than return it.` – Mees Nov 07 '22 at 10:31
36

There should be a container that maps component names to all components that are supposed to be used dynamically. Component classes should be registered in a container because in modular environment there's otherwise no single place where they could be accessed. Component classes cannot be identified by their names without specifying them explicitly because function name is minified in production.

Component map

It can be plain object:

class Foo extends React.Component { ... }
...
const componentsMap = { Foo, Bar };
...
const componentName = 'Fo' + 'o';
const DynamicComponent = componentsMap[componentName];
<DynamicComponent/>;

Or Map instance:

const componentsMap = new Map([[Foo, Foo], [Bar, Bar]]);
...
const DynamicComponent = componentsMap.get(componentName);

Plain object is more suitable because it benefits from property shorthand.

Barrel module

A barrel module with named exports can act as such map:

// Foo.js
export class Foo extends React.Component { ... }

// dynamic-components.js
export * from './Foo';
export * from './Bar';

// some module that uses dynamic component
import * as componentsMap from './dynamic-components';

const componentName = 'Fo' + 'o';
const DynamicComponent = componentsMap[componentName];
<DynamicComponent/>;

This works well with one class per module code style.

Decorator

Decorators can be used with class components for syntactic sugar, this still requires to specify class names explicitly and register them in a map:

const componentsMap = {};

function dynamic(Component) {
  if (!Component.displayName)
    throw new Error('no name');

  componentsMap[Component.displayName] = Component;

  return Component;
}

...

@dynamic
class Foo extends React.Component {
  static displayName = 'Foo'
  ...
}

A decorator can be used as higher-order component with functional components:

const Bar = props => ...;
Bar.displayName = 'Bar';

export default dynamic(Bar);

The use of non-standard displayName instead of random property also benefits debugging.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
20

With the introduction of React.lazy, we can now use a true dynamic approach to import the component and render it.

import React, { lazy, Suspense } from 'react';

const App = ({ componentName, ...props }) => {
  const DynamicComponent = lazy(() => import(`./${componentName}`));
    
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <DynamicComponent {...props} />
    </Suspense>
  );
};

This approach makes some assumptions about the file hierarchy of course and can make the code easy to break.

jchapron
  • 201
  • 2
  • 3
  • shouldn't your return statement be returning ``? It looks like you edited the component name on line 4 but not on line 8 – jymbob Dec 07 '20 at 10:07
14

Across all options with component maps I haven't found the simplest way to define the map using ES6 short syntax:

import React from 'react'
import { PhotoStory, VideoStory } from './stories'

const components = {
    PhotoStory,
    VideoStory,
}

function Story(props) {
    //given that props.story contains 'PhotoStory' or 'VideoStory'
    const SpecificStory = components[props.story]
    return <SpecificStory/>
}
Stalinko
  • 3,319
  • 28
  • 31
12

I figured out a new solution. Do note that I am using ES6 modules so I am requiring the class. You could also define a new React class instead.

var components = {
    example: React.createFactory( require('./ExampleComponent') )
};

var type = "example";

newComponent() {
    return components[type]({ attribute: "value" });
}
Sam
  • 4,205
  • 2
  • 17
  • 13
10

For a wrapper component, a simple solution would be to just use React.createElement directly (using ES6).

import RaisedButton from 'mui/RaisedButton'
import FlatButton from 'mui/FlatButton'
import IconButton from 'mui/IconButton'

class Button extends React.Component {
  render() {
    const { type, ...props } = this.props

    let button = null
    switch (type) {
      case 'flat': button = FlatButton
      break
      case 'icon': button = IconButton
      break
      default: button = RaisedButton
      break
    }

    return (
      React.createElement(button, { ...props, disableTouchRipple: true, disableFocusRipple: true })
    )
  }
}
Ricardo Pedroni
  • 1,772
  • 17
  • 10
8

If your components are global you can simply do:

var nameOfComponent = "SomeComponent";
React.createElement(window[nameOfComponent], {});
Ray
  • 2,713
  • 3
  • 29
  • 61
  • 1
    This works beautifully, especially if using Rails. The accepted answer does not work for me, because the `Components` array is not defined. – Vadim Apr 28 '16 at 09:22
  • 4
    Rather than attach arbitrarily named objects to the global scope (euw), you should consider maintaining a component registry that you can register and then retrieve component references from when needed. – slashwhatever Nov 01 '16 at 17:01
  • `window[nameOfComponent]` is unnecessary here and can be replaced by `nameOfComponent` – Nate-Wilkins Oct 28 '22 at 12:58
  • why mentioning global var window, in react window['name'] will be undefined – Nuha Dzikri Jun 21 '23 at 16:18
4

Having a map doesn't look good at all with a large amount of components. I'm actually surprised that no one has suggested something like this:

var componentName = "StringThatContainsComponentName";
const importedComponentModule = require("path/to/component/" + componentName).default;
return React.createElement(importedComponentModule); 

This one has really helped me when I needed to render a pretty large amount of components loaded in a form of json array.

semaj1919
  • 87
  • 7
Arthur Z.
  • 183
  • 1
  • 5
  • This is close to what worked for me, and led me in the right direction. Trying to invoke `React.createElement(MyComponent)` directly threw an error. Specifically, I don't want the parent to have to know all of the components to import (in a mapping) - because that seems like an extra step. Instead, I used `const MyComponent = require("path/to/component/" + "ComponentNameString").default; return `. – semaj1919 Jun 04 '20 at 21:45
3

Assume we have a flag, no different from the state or props:

import ComponentOne from './ComponentOne';
import ComponentTwo from './ComponentTwo';

~~~

const Compo = flag ? ComponentOne : ComponentTwo;

~~~

<Compo someProp={someValue} />

With flag Compo fill with one of ComponentOne or ComponentTwo and then the Compo can act like a React Component.

AmerllicA
  • 29,059
  • 15
  • 130
  • 154
1

Assuming you are able to export * from components like so...

// src/components/index.js

export * from './Home'
export * from './Settings'
export * from './SiteList'

You can then re-import * into a new comps object, which can then be used to access your modules.

// src/components/DynamicLoader.js

import React from 'react'
import * as comps from 'components'

export default function ({component, defaultProps}) {
  const DynamicComponent = comps[component]

  return <DynamicComponent {...defaultProps} />
}

Just pass in a string value that identifies which component you want to paint, wherever you need to paint it.

<DynamicLoader component='Home' defaultProps={someProps} />
Carey
  • 71
  • 4
0

Suspose we wish to access various views with dynamic component loading.The following code gives a working example of how to accomplish this by using a string parsed from the search string of a url.

Lets assume we want to access a page 'snozberrys' with two unique views using these url paths:

'http://localhost:3000/snozberrys?aComponent'

and

'http://localhost:3000/snozberrys?bComponent'

we define our view's controller like this:

import React, { Component } from 'react';
import ReactDOM from 'react-dom'
import {
  BrowserRouter as Router,
  Route
} from 'react-router-dom'
import AComponent from './AComponent.js';
import CoBComponent sole from './BComponent.js';

const views = {
  aComponent: <AComponent />,
  console: <BComponent />
}

const View = (props) => {
  let name = props.location.search.substr(1);
  let view = views[name];
  if(view == null) throw "View '" + name + "' is undefined";
  return view;
}

class ViewManager extends Component {
  render() {
    return (
      <Router>
        <div>
          <Route path='/' component={View}/>
        </div>
      </Router>
    );
  }
}

export default ViewManager

ReactDOM.render(<ViewManager />, document.getElementById('root'));
1-14x0r
  • 1,697
  • 1
  • 16
  • 19
0

You can create a reusable component with a fallback component.

export const StringComponent = (Base, { name, Fallback = undefined, ...rest }) => {
    const Component = Base[name];

    // return fallback if the component doesn't exist
    if (!Component) return <Fallback/>

    return <Component {...rest}/>;
};

And call it like this:

import * as Pages from "../pages"

const routes = [
  {path: "/", element: "Home" },
  {path: "/about", element: "About" },
  {path: "*", element: "NotFound" },
]

export function App(){
  
  const Fallback = Pages.NotFound

  // render each route using a string as name
  return (
    <div>
      { 
        routes.map(page => 
          StringComponent(Pages, { name: page.element, Fallback }) 
        )
      }
    </div>
  )
}

OBS: Imported Pages needs to be something like this:

import Home from "./home"
import About from "./about"
import NotFound from "./not-found"

export { Home, About, NotFound }
Kuza Grave
  • 1,256
  • 14
  • 15
0

Say, your components are defined in Components.js:

import React from 'react';

class Foo extends React.Component {
    render () {
        return (
            <div>
                <p>Foo is best</p>
            </div>
        );
    }
}

class Bar extends React.Component {
    render () {
        return (
            <div>
                <p>Bar is Bar</p>
            </div>
        );
    }
}

export {
    Foo,
    Bar
};

You can import * as X from Y and all component classes will be put into a dictionary X. Then render them from string this way:

import React from 'react';
    
import * as componentClasses from './Components';

class App extends React.Component {
    render () {
        const componentClass = componentClasses['Foo']; /** 'Foo' or 'Bar' **/
        const component = React.createElement(componentClass, {});

        return (
            <div>
                <h1>Cool</h1>
                {component}
            </div>
        );
    }
}

export default App;

https://codesandbox.io/s/unruffled-maria-i8psmo?file=/src/App.js

4pie0
  • 29,204
  • 9
  • 82
  • 118
-3

I used a bit different Approach, as we always know our actual components so i thought to apply switch case. Also total no of component were around 7-8 in my case.

getSubComponent(name) {
    let customProps = {
       "prop1" :"",
       "prop2":"",
       "prop3":"",
       "prop4":""
    }

    switch (name) {
      case "Component1": return <Component1 {...this.props} {...customProps} />
      case "Component2": return <Component2 {...this.props} {...customProps} />
      case "component3": return <component3 {...this.props} {...customProps} />

    }
  }
Ray
  • 2,713
  • 3
  • 29
  • 61
abhirathore2006
  • 3,317
  • 1
  • 25
  • 29
  • 1
    Just came across this again. This is the way to do it. You always know your components anyway, and need to load them. So this is a great solution. Thanks. – Jake Jun 20 '16 at 06:09
-3

Edit: Other answers are better, see comments.

I solved the same problem this way:

...
render : function () {
  var componentToRender = 'component1Name';
  var componentLookup = {
    component1Name : (<Component1 />),
    component2Name : (<Component2 />),
    ...
  };
  return (<div>
    {componentLookup[componentToRender]}
  </div>);
}
...
Hammad Akhwand
  • 779
  • 1
  • 7
  • 19
  • 3
    You probably don't want to do this because `React.createElement` will be invoked for every component in your lookup object, even though only one of them is being rendered at a time. Worse, by putting the lookup object in the `render` method of the parent component, it will do it again every time the the parent is rendered. The top answers are a much better way of achieving the same thing. – Inkling Apr 09 '17 at 03:24