Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to render component from name string #3365

Closed
charliegeiger89 opened this issue Mar 10, 2015 · 17 comments
Closed

Unable to render component from name string #3365

charliegeiger89 opened this issue Mar 10, 2015 · 17 comments

Comments

@charliegeiger89
Copy link

I seem to be unable to render components using name strings. I wanted to be able to dynamically generate component name strings which have existing corresponding components that can be rendered.

Fiddle: http://jsfiddle.net/dhjxu5oL/

Instead of referencing the existing component and rendering it, React is rendering a custom element tag that is all lower case.

// HTML output
<componentname></componentname>

Very basic code that is failing:

// Main component to be rendered
var Parent = React.createClass({

    render: function() {
        var ChildName = 'Child';
        return (
            <ChildName />
        );
    }

});

// Dynamic subcomponent render
var Child = React.createClass({

    render: function() {
        return (
            <div>
                I am Child!
            </div>
        );
    }

});

React.render(
    <Parent/>,
    document.body
);
@sophiebits
Copy link
Contributor

Simple fix: just use Child instead of 'Child' – you want to pass an actual reference to the JS variable:

http://jsfiddle.net/dhjxu5oL/1/

@jimfb
Copy link
Contributor

jimfb commented Mar 10, 2015

@spicyj I don't think that's what he is asking. I think he wants to be able to create a React element from a dynamically acquired name (string).

What he is looking for is:

React.createElement(ChildName, null)

instead of what he has (<ChildName />)

@sophiebits
Copy link
Contributor

React.createElement(ChildName, null) and <ChildName /> are equivalent.

@jimfb
Copy link
Contributor

jimfb commented Mar 10, 2015

@spicyj EDIT: You're correct.

@charliegeiger89
Copy link
Author

@spicyj @JSFB Both of those suggestions arrive at the same outcome. Neither of them are supporting the ability to dynamically generate name strings which reference existing components.

I wanted to store some ID value in a component prop and then use the name string to render the component.

What I am looking for is something almost like eval() or running window[nameString]().

var NameString = 'Component' + this.props.componentID;
return <NameString />

Updated fiddle with easy testing: http://jsfiddle.net/q3zwtosw/

@sophiebits
Copy link
Contributor

As you probably know, it's best practice not to use eval or look up globals on window by string name as it tends to make code hard to reason about, but you can use those patterns with React too:

var name = 'Component' + this.props.componentID;
var Component = window[name];  // or eval(name)
return <Component />;

The key is to use a capitalized variable name (Component) or else React will treat it as a built-in DOM element.

@charliegeiger89
Copy link
Author

I am aware of evils of eval / window lookups, I just assumed/hoped React would support something like a secure scoped reference method as part of JSX transpiling. One could easily implement an immutable scoped object of valid component names and then compare strings to ensure that nothing malicious was going to be passed to the lookup method. It would greatly simplify dynamic component calls by not requiring massive switch or else if statements in front of render return statements.

@spicyj @JSFB Thanks!

@sophiebits
Copy link
Contributor

Maybe you can do something like

var allMyComponents = {};

var Child = React.createClass(...);
allMyComponents['Child'] = Child;

// ...
var Component = allMyComponents[name];

which isn't a large overhead but is more reasonable than using globals. React tries to be simple and get out of your way instead of adding a lot of magic – you can then build whatever you want on top, just as you can in JS without React.

@charlie0077
Copy link

@charliegeiger89: what is your final solution for this issue?

I am having the same problem as yours. I have tried window[myStringVal], however, it fails somehow in Firefox.
The following code posted by spicyj seems not address the problem, because Child is unknown when the code is written. Wondering why React does not support variable string name as component name.

var allMyComponents = {};

var Child = React.createClass(...);
allMyComponents['Child'] = Child;

// ...
var Component = allMyComponents[name];

@tmikoss
Copy link

tmikoss commented Dec 9, 2015

Came across this issue today, from a bit different point of view: configuration.

I'm creating an embeddable element that can receive configuration options - one of them being what react class to use when rendering.

Example:

<EmbeddableList>
  <ListElement />
  <ListElement />
  <ListElement />
  ...
</EmbeddableList>

I want to be able to pass a listElementClass prop to EmbeddableList that changes what react class to use instead of ListElement when rendering. In EmbeddableList render method I call React.createElement(this.props.listElementClass, someProps).

There are no problems if the react class object gets passed in as listElementClass. But I'm working with react-rails, and the intended use case is calling react_component 'EmbeddableList', { listElementClass: 'DifferentListElement' } Rails helper, and that limits me to prop types that can be represented in JSON. That means I'm receiving string 'DifferentListElement' as a prop, instead of the actual class.

Currently I'm using eval(this.props.listElementClass) to get the actual react class. It works, but leaves a bad taste in my mouth. I was hoping React.createFactory would be of use here, but that also wants the actual class, instead of string.

@zpao
Copy link
Member

zpao commented Dec 9, 2015

React doesn't have a global registry so that won't ever be built-in functionality.

You could have your own registry and just make sure each class registers in that, then you could access that inside EmbeddableList, then you can just use that. For example:

// DifferentListElement.js.jsx
var DifferentListElement = React.creaceClass({});
ReactComponentRegistry.set('DifferentListElement', DifferentListElement);

// EmbeddableList.js.jsx
var EmbeddableList = React.creaceClass({
  render() {
    var ListElement = ReactComponentRegistry.get(this.props.listElementClass)
    return <Whatever><ListElement>...
  }
});

This is essentially what Ben described above, but with a registry object. Either way it would require a different global object be available.

The other thing is that you may be setting window.DifferentListElement when creating the class because that's the default behavior of the asset pipeline. So when you're doing eval(this.props.listElementClass), you could actually just write window[this.props.listElementClass] and would be a slightly less bad taste.

React.createFactory is only for when you aren't using JSX, and you still need the class object itself to pass in.

@afilp
Copy link

afilp commented May 11, 2016

I came twice in a situation that I needed a dynamic use of the proper component.

A suggestion below:

The React team could create a helper function, for example: React.getComponentByName(name: string), that returns the component that has the same name as the input string:

import MyComponent from './MyComponent.js';
const myComponents = [];
myComponents[0] = React.getComponentByName(`MyComponent`);

In that way, we can easily implement the following use case of rendering the proper component based on props received:

import MyComponent1 from './MyComponent1.js';
import MyComponent2 from './MyComponent2.js';
import MyComponent3 from './MyComponent3.js';

const myComponentsList = [];

for (let i of [1, 2, 3]) {
    myComponentsList[i] = React.getComponentByName(`MyComponent${i}`);
}

Then, in JSX:

render()
{
    return (
            <div>

                <div>
                    Use the proper Component below, based on the incoming props:
                </div>

                    {
                       myComponentsList[this.props.componentId]
                    }

            </div>
    )
}

@syranide
Copy link
Contributor

@af7 If that's what you want then you need to provide the mapping yourself, there is nothing React can do for you here. React works with regular classes and classes are not magically registered with React.

import MyComponent1 from './MyComponent1.js';
import MyComponent2 from './MyComponent2.js';
import MyComponent3 from './MyComponent3.js';

const myComponentsList = [
  MyComponent1,
  MyComponent2,
  MyComponent3
];

const myComponentsListByName = {
  MyComponent1: MyComponent1,
  MyComponent2: MyComponent2,
  MyComponent3: MyComponent3
};

/* es6 syntax */
const myComponentsListByName = {
  MyComponent1,
  MyComponent2,
  MyComponent3
};

@sophiebits
Copy link
Contributor

sophiebits commented May 11, 2016

What @syranide said. Then in render you can do:

// Needs to start with a capital letter
var Type = myComponentsList[this.props.componentId];
...
<Type />

@rmoskal
Copy link

rmoskal commented Jun 4, 2016

If you can live with having all your components in one module, then this works pretty well:

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

@skflowne
Copy link

skflowne commented Jun 9, 2016

From @rmoskal said, I've been able to put something together. I'm also using react-rails but with a different setup you can find here if you're on Rails as well it will allow you use import statements and use npm packages : Rails + Browserify + React + ES7

I created a selectr_option_templates file where I will put the different components I want to use as templates which looks like this :

class ProductColor extends React.Component {

    render() {
        return (
            <div className="product-color">
                <span style={{background: this.props.option.value}}></span> {this.props.option.name}
            </div>
        );
    }
}

ProductColor.propTypes = {
    option: React.PropTypes.object
}

module.exports = {
    "ProductColor": ProductColor
}

The important part then is to use module.exports to make the mapping happen.

Then in your parent module you can do :

import * as templates from 'components/selectr/selectr_option_templates';

var optionComponent = templates[this.props.optionComponent]

<optionComponent />

I pass "ProductColor" in my this.props.optionComponent
This way I can get my class name from the props, previously set with react_component helper with react-rails. Obviously this will work however you get your string class name from.

This is the best solution I found so far, also for some reason eval(this.props.optionComponent) throws an error so I can't use it.

@agrawalaayushi
Copy link

@afilp, this solution is not favorable to Tree Shaking

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests