31

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.

António Ribeiro
  • 4,129
  • 5
  • 32
  • 49
madCode
  • 313
  • 1
  • 3
  • 8

7 Answers7

23

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} />
  );
}
faceyspacey.com
  • 2,510
  • 2
  • 20
  • 29
  • 1
    If you can lazy load files that are webpacked you could probably also tweak the loader to load any js file? – Bill Jun 22 '17 at 22:26
  • yes. you can do dynamic requires, but they won't work with SSR with packages like https://github.com/faceyspacey/react-universal-component or React Loadable until webpack allows for dynamic requires in their `require.resolveWeak` function. Vote up and comment my issue here to get this sooner than later: https://github.com/webpack/webpack/issues/4993 – faceyspacey.com Jun 23 '17 at 07:05
  • The solution here is better: https://stackoverflow.com/questions/48268507/react-dynamically-import-components – Crouching Kitten Aug 05 '18 at 18:01
8

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;
Saman
  • 5,044
  • 3
  • 28
  • 27
  • I tried the same its not working, Error: Cannot find module "." Is there any working repo?? since i am trying in all possible ways but still getting the same. – Lionel Dcosta Nov 02 '17 at 07:43
  • 2
    @LionelDcosta I have provided the code https://gist.github.com/SamanShafigh/a0fbc2483e75dc4d6f82ca534a6174d4 But please do more study to find the drawbacks of this approach, I am not sure if it is good for big size projects – Saman Nov 03 '17 at 04:05
4

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

danialk
  • 1,195
  • 11
  • 32
1

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.

Ciaran George
  • 571
  • 5
  • 19
1
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" />
Muhammad Usman
  • 10,426
  • 22
  • 72
  • 107
0

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

Charli
  • 41
  • 2
0

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
cansu
  • 958
  • 1
  • 12
  • 23
  • Even though it is a good solution, be aware it is not compatible with React 17. "Warning: Legacy context API has been detected within a strict-mode tree. The old API will be supported in all 16.x releases, but applications using it should migrate to the new version. Please update the following components: LoadableComponent" – The scion Feb 08 '22 at 10:05