madCode
madCode

Reputation: 323

Dynamic loading of react components

I need to load a react component dynamically.

I get name of the component to load as a string from user. I'm using webpack.

How do I load the component dynamically instead of having a static import statement. It seems Require.Ensure doesn't evaluate expressions . What I want to achieve is something like this.

require.ensure([ "./widgets/" + componentName ] ,(require) => {
    let Component = require("./widgets/" + componentName);   
});

But this doesn't seem to work.

Upvotes: 32

Views: 50686

Answers (7)

cansu
cansu

Reputation: 1142

I was searching for a solution, then i found out react-router npm package.

https://www.npmjs.com/package/react-loadable

Here is my example :

Loading component with name dynamically. First get loadables :

  const MyComponent = Loadable({
    loader: () => import(`../charts/${some_name}`),
    loading: () => <div>Loading...</div>,
  });

Then you have it, you can render it :

<MyComponent />

This reading may help also :

https://itnext.io/react-loading-components-dynamically-a9d8549844c4

Upvotes: 0

Charli
Charli

Reputation: 61

There are a lot of good answers here. Another approach I found is to lazy load the component as a micro app. The host application does not even need to install or import the component at design time.

import React from 'react';
import ReactDOM from 'react-dom';
import MicroApp from '@schalltech/honeycomb-react-microapp';

const App = () => {
  return (
    <MicroApp
        config={{
          View: {
            Name: 'redbox-demo',
            Scope: 'beekeeper',
            Version: 'latest'
          }
        }}
      />
  );
});

export default App;

I experimented with this by following along with the link below. Its a pretty interesting concept.

https://github.com/Schalltech/honeycomb-marketplace

Upvotes: 0

danialk
danialk

Reputation: 1453

As of React 16.6.0 (October 23, 2018), React.lazy() may be a convenient solution, under the banner of code-splitting for client-side rendering.

Fixed component names

Given the following component you wish to lazy load (MyLazy.js):

import React from 'react';

export default function (props) {
  return (
    <p>My component</p>
  );
}

You could render this from another component (say App.js) with the following, noting that the built-in <Suspense> component will display until loading is complete:

import React, { Suspense } from 'react';

export function App(props) {
  const MyLazyLoaded = React.lazy(() => import('./MyLazy'));
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <MyLazyLoaded />
      </Suspense>
    </div>
  );

}

Dynamic component names

If you want to dynamically set the name of the component, e.g. from user input per your question, you could make the import statement variable, e.g.:

var lazyComponentName = 'MyLazy';
const MyLazyLoaded = React.lazy(() => import('./' + lazyComponentName));

Note that if you do not concatenate and use the variable name directly(i.e. import(lazyComponentName)), you will be presented with the following ES Lint warning, related to webpack:

Critical dependency: the request of a dependency is an expression

Upvotes: 4

Muhammad Usman
Muhammad Usman

Reputation: 10936

import React, { Component } from "react";

class DynamicLoader extends Component {

  render() {
    const COMPONENTS = {
      CompA: require('./CompA'),
      CompB: require('./CompB'),
    }
    if(this.props.component && COMPONENTS[this.props.component]) {
      let Component = COMPONENTS[this.props.component].default;
      return <Component />
    }
  }
}

export default DynamicLoader;
<DynamicLoader component="CompA" />

Upvotes: 1

Ciaran George
Ciaran George

Reputation: 613

I had a different solution due to my issue being a little different, I looked for an answer for a while but couldn't find one. Hopefully this may help someone.

My issue was that we had several applications deployed independently as different modules within a core UI. These had to be completely independently deployable within having any effect on the others.

My solution was to have the sub component applications wrapped as web pack libraries by setting these webpack properties:

libraryTarget: 'umd',
globalObject: 'this',
library: 'SubComponentName'

I think provided 2 entry points for the application based on it being accessed either directly or through a parent component within a separate application

class Library extends Component {

    render() {
        return (
            < Provider store = {store} >< BrowserRouter>
                < App isAccessDirect={this.props && this.props.isAccessDirect || false} roles={this.props.roles}>
                </App>
            < /BrowserRouter>< /Provider>
        )
    }
}

class Launcher extends Component {
    render() {
        return (
            ReactDOM.render(
                <Library isAccessDirect="true"/>,
                document.getElementById('root')
            )
        )
    }
}

Once this app is built I can access it directly in the index.html with

<script>
    new Compression.Launcher().render();
</script>

Or if you wish to access the application via a separate host / component as was my requirement you can do so by using a combination of loadjs and react.

loadjs(['../sub/component/bundle.min.js'], 'loadSubComponent');

var subComponent = this.getUI('subComponentWrapperElement').get(0);

loadjs.ready('loadSubComponent', function() {

    var roles = my.app.roles;

    ReactDOM.render(
        <SubComponentName.Library roles={roles}></SubComponentName.Library>,
        subComponent
    );
});

This can load in a react component in any type of javascript client side framework, in our case we used backbone within the wrapper application. The bundle.min.js was also located behind a reverse proxy but it could be located anywhere, its just essential that you load it in and await the load to finish, which loadjs was perfect for in this case. One final important bit of information was that destruction of the component was vital for ensuring a good user experience, as without we encountered a number of issues.

loadjs.reset();
var subComponent = this.getUI('subComponentWrapperElement').get(0);
ReactDOM.unmountComponentAtNode(subComponent);

So in a nut shell what I feel this provides over the other answers is a completely independently deployable suite of applications that can be completely framework agnostic.

Upvotes: 1

Saman
Saman

Reputation: 5334

Please have a look at this gist page that I have provided for the full source code https://gist.github.com/SamanShafigh/a0fbc2483e75dc4d6f82ca534a6174d4

So let assume you have 4 components called D1, D2, D3. What you need is to create a dependency injection and a dependency container mechanism. This is a very simple implementation

Imagine you have a config file like this that defines your components

export default [
  {
    name:'D1',
    path:'D1'
  },
  {
    name:'D2',
    path:'D2'
  },
  {
    name:'D3',
    path:'D3'
}];

Then you can have a component container something like this

import componentsConfig from 'ComponentsConfig';

let components = {};

for (var i = 0; i < componentsConfig.length; i++) {
  let componentConfig = componentsConfig[i];
  // Check if component is not already loaded then load it
  if (components[componentConfig.name] === undefined) {
    components[componentConfig.name] = require(`${componentConfig.path}`).default;
  }
}

export default components;

Finally in the place you want to load your components you can use your component container to load your components dynamically or in other word you can inject your components

import React, { Component } from 'react';
import ComponentContainer from './ComponentContainer';

class App extends Component {
  render() {
    let components = ['D1', 'D2', 'D3'];

    return (
      <div>
        <h2>Dynamic Components Loading</h2>
        {components.map((componentId) => {
          let Component = ComponentContainer[componentId];
          return <Component>{componentId}</Component>;
        })}
      </div>
    );
  }
}

export default App;

Upvotes: 8

faceyspacey.com
faceyspacey.com

Reputation: 2630

Basically it boils down to pre-creating all the chunks you will ever need. Then you just need a way to dynamically refer to them. Here's the solution I based mine on:

http://henleyedition.com/implicit-code-splitting-with-react-router-and-webpack

and here's what I do since I don't use React Router (side note: i dont find it to be a good match for redux or animations):

//loader:
{
  test: (folder)\/.*\.js,
  include: path.resolve(__dirname, 'src')
  loader: ['lazy?bundle', 'babel']
}

//dynamic usage within React component:
const My_COMPONENTS = {
   ComponentA: require('./folder/ComponentA'),
   ComponentB: require('./folder/ComponentB'),
}

class ParentComponent extends React.Component {
    componentDidMount() {
        My_COMPONENTS[this.props.name](component => this.setState({component}));
    } 
    render() {
       return <this.state.component />;
    }
}

So the result is you are dynamically rendering a component, but from a static pre-determined set of possibilities--all while, only sending no more to the client than the chunks the visitor is actually interested in.

ALSO, here's a component I have that does this well:

import React from 'react';
import Modal from './widgets/Modal';

export default class AjaxModal extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = {
      Content: null
    };
  }

  componentDidMount() {
    if(this.props.show) {
      this.loadContent();
    }
  }

  componentWillReceiveProps({show}) {
    if(show && !this.state.Content) {
      this.loadContent(1200); //dont interfere with animation
    }
  }

  loadContent(ms=0) {
    setTimeout(() => {
      this.props.requestLazyBundle(({default: Content}) => {
        this.setState({Content});
      });
    }, ms);
  }

  render() {
    let {Content} = this.state;

    return (
      <Modal title={this.props.title} {...this.props} loading={!Content}>
        {Content ? <Content /> : null}
      </Modal>
    );
  }
}

pass pass an async require bundler function as this.props.requestLazybundle, like this:

render() {

  let requestLazyBundle = require('bundle?lazy&name=AnotherComponent!../content/AnotherComponent');

  return (
    <AjaxModal title='Component Name' {...props} requestLazyBundle={requestLazyBundle} />
  );
}

Upvotes: 23

Related Questions