Instantiating Components by Name in ReactJS
A useful pattern in designing React apps is to drive the instantiation of components from data. That is, instead of the application instantiating the components and then fetching data to fill them, the application fetches data and the data itself specifies which components are best to display that data.
For example, suppose I have an application that is a news feed. That feed can have many kinds of stories. And I can fetch each story in the feed by an id:
get http://feedserver.com/story?id=12345 { "type": "WidgetOne", "content": { ... } } get http://feedserver.com/story?id=12346 { "type": "widgetThree", "content": { ... } } ... etc ...
Then in the application code we define the widget components that can display each kind of story:
class WidgetOne extends React.Component { ... } class WidgetTwo extends React.Component { ... } class WidgetThree extends React.Component { ... }
Now we just have to figure out a way to instantiate each widget from the string in the data. If the components are defined in global scope, as they would be in a plain JS file, they are all children of the global scope window
. So I can return the class object from a string by just looking up that key in window: window["WidgetOne"]
.
So for example suppose I have a function that fetches stories and instantiates components:
function getContent( url ) { fetch( url ).then ( r => r.json().then( data => React.createElement( window[data.type], data ); ); ); }
This is just a toy example to illustrate the idea. In reality you’d need to return a promise, or route the returned data through Redux or some other mechanism so that the instantiated component can be put in the right place.
The the core of the idea is that the type string in the returned data is used to select the component type to use to instantiate that data.
Using this with a packager like Browserify, Webpack or Rollup
One problem with this that the component objects will not be in global scope if your application is packaged up in a packager that uses iife
(immediately-invoked function expression). This is where your application is packaged up as a self executing function, a great choice for web apps that execute in a browser. This is what it looks like compiled:
(function () { class WidgetOne extends React.Component { ... } class WidgetTwo extends React.Component { ... } class WidgetThree extends React.Component { ... } function getContent( url ) { fetch( url ).then ( r => r.json().then( data => React.createElement( window[data.type], data ); // fail! ); ); } }());
Since the component classes are created in the scope of the iife
function, they are no longer members of window
, and the window expression fails.
A good answer in this case is to package up all your widgets in an ES6 module and import them as a single object.
// widget.js export class WidgetOne extends React.Component { ... } export class WidgetTwo extends React.Component { ... } export class WidgetThree extends React.Component { ... }
and then in your main script:
import * as Widgets from "./widgets.js" function getContent( url ) { fetch( url ).then ( r => r.json().then( data => React.createElement( Widgets[data.type], data ); // works again! ); ); }
Resources:
Create an instance of a React class from a string | A StackOverflow answer describing this and other techniques |
Unable to render component from name string | A discussion of this issue on github forum for React |