Dynamically loading React components

I'm thinking about building a web application, where people can install plugins. I'd like plugins to be able to define React components that will be rendered to the page, without recompiling the main JavaScript bundle after installing the plugin.

So here's the approach I'm thinking of:

  • Bundle the main JavaScript with React as an external library, using webpack.
  • Have plugin authors compile their components with React as an external library as well.

This way, I'm only running one instance of React. I could probably do the same with some other frequently used libraries.

The problem then, is how to dynamically load these plugin components from the server. Let's say I have the following component:

class PluginRenderer extends React.Component{
  componentWillMount() {
    getPluginComponent(`/plugins/${this.props.plugin}/component.js`).then((com) => {
      this.setState({pluginComponent: com});
    })
  }

  render() {
    var Plugin = this.state.pluginComponent;
    return Plugin ? <Plugin {...this.props} /> : "Loading..."
  }
}

How could getPluginComponent be implemented?

Answers:

Answer

It's an interesting problem I also faced some months ago for customer work, and I didn't see too many document approaches out there. What we did is:

  1. Individual plugins will be separate Webpack projects, for which we provide either a template or a CLI tool that generates project templates.

  2. In this project we define Webpack externals for shared vendor libraries already used in the core application: React, Redux, etc. This tells the plugin to not include those in the bundle but to grab them from a variable in window we set in the core app. I know, sounds like sucks, but it's much better than having all plugins re-include 1000s of shared modules.

  3. Reusing this concept of external, the core app also provides some services via window object to plugins. Most important one is a PluginService.register() method which your plugin must call when it's initialized. We're inverting control here: the plugin is responsible to say "hi I'm here, this is my main export (the Component if it's a UI plugin)" to the core application.

  4. The core application has a PluginCache class/module which simply holds a cache for loaded plugins (pluginId -> whatever the plugin exported, fn, class, whatever). If some code needs a plugin to render, it asks this cache for it. This has the benefit of allowing to return a <Loading /> or <Error /> component when a plugin did not load correctly, and so on.

  5. For plugin loading, this PluginService/Manager loads the plugin configuration (which plugins should I load?) and then creates dynamically injected script tags to load each plugin bundle. When the bundle is finished, the register call described in step 3 will be called and your cache in step 4 will have the component.

  6. Instead of trying to load the plugin directly from your component, ask for it from the cache.

This is a very high level overview which is pretty much tied to our requirements back then (it was a dashboard-like application where users could add/remove panels on the fly, and all those widgets were implemented as plugins).

Depending on your case, you could even wrap the plugins with a <Provider store={ theCoreStore }> so they have to access to Redux, or setup an event bus of some kind so that plugins can interact with each other... There is plenty of stuff to figure out ahead. :)

Good luck, hope it helped somehow!

Answer

I presented an alternative approach on a similar question. Recapping:

On your app

import(/* webpackIgnore: true */'https://any.url/file.js')
  .then((plugin) => {
    plugin.main({ /* stuff from app plugins need... */ });
  });

On your plugin...

const main = (args) => console.log('The plugin was started.');
export { main };
export default main;

See more details on the other question's page.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.