Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically inject data in React Router Routes

I've been working on trying to modularize my React.js app (that will be delivered as a Desktop app with Electron) in a way that if I make a new module in the future, I can just add a new folder and modify a couple of files and it should integrate fine. I got originally inspired by this article: https://www.nylas.com/blog/react-plugins/

After that point, I started doing as much research as I could and ended up creating a JSON file that would live in the server with a manifest of the plugins that are registered for that specific client. Something like this:

{
    "plugins": [
        {
            "name": "Test Plugin",
            "version": "0.0.1",
            "path": "testplugin",
            "file": "test",
            "component":"TestPlugin"
        },
        {
            "name": "Another Plugin",
            "version": "0.0.1",
            "path": "anothertest",
            "file": "othertest",
            "component":"TestPluginDeux"
        }
    ]
}

After that, I made a couple folders that match the path value and that contain a component that matches the name in the manifest (e.g. testplugin/test.jsx that exports the TestPlugin component as a default). I also made a pluginStore file that reads the manifest and mounts the plugins in the this.state.

Then, did a ton of research on Google and here and found this answer: React - Dynamically Import Components

With that function, I was able to iterate through the manifest, find the folders in the directory, and mount the plugins in the this.state by running the mountPlugins() function I had created in the pluginStore, inside a componentDidMount() method in my homepage.

So far so good. I'm using React-Router and I was able to mount the plugins dynamically in the State and able to load them in my Home Route by just calling them like this: <TestPlugin />.

The issue that I have now, is that I wanted to dynamically create Routes that would load these components from the state, either by using the component or the render method, but I had no luck. I would always get the same result... Apparently I was passing an object instead of a String.

This was my last iteration at this attempt:

{this.state.modules.registered.map((item) =>
<Route exact path={`/${item.path}`} render={function() {
  return <item.component />
  }}></Route>
)}

After that, I made a Route that calls a PluginShell component that is called by a Navlink that sends the name of the plugin to inject and load it dynamically.

<Route exact path='/ex/:component' component={PluginShell}></Route>

But I ended having the same exact issue. I'm passing an object and the createElement function expected a string.

I searched all over StackOverflow and found many similar questions with answers. I tried applying all the possible solutions with no luck.

EDIT: I have put together a GitHub repo that has the minimal set of files to reproduce the issue.

Here's the link: https://codesandbox.io/embed/aged-moon-nrrjc

like image 401
Alfie Robles Avatar asked Aug 15 '19 16:08

Alfie Robles


2 Answers

You've got the right idea, if anything I guess your syntax is slightly off. I didn't have to tweak much from your example to get dynamic routing work.

Here's a working example of what I think you want to do:

const modules = [{
  path: '/',
  name: 'Home',
  component: Hello
},{
  path: '/yo',
  name: 'Yo',
  component: Yo
}];


function DynamicRoutes() {
    return (
        <BrowserRouter>
            { modules.map(item => <Route exact path={item.path} component={item.component}/>) }
        </BrowserRouter>
    );
}

https://stackblitz.com/edit/react-zrdmcq

like image 20
UncleDave Avatar answered Sep 20 '22 17:09

UncleDave


Okey pokey. There are a lot of moving parts here that can be vastly simplified.

  1. I'd recommend moving toward a more developer-friendly, opinionated state store (like Redux). I've personally never used Flux, so I can only recommend what I have experience with. As such, you can avoid using plain classes for state management.
  2. You should only import the modules ONCE during the initial application load, then you can dispatch an action to store them to (Redux) state, then share the state as needed with the components (only required if the state is to be shared with many components that are spread across your DOM tree, otherwise, not needed at all).
  3. Module imports are asynchronous, so they can't be loaded immediately. You'll have to set up a condition to wait for the modules to be loaded before mapping them to a Route (in your case, you were trying to map the module's registered string name to the route, instead of the imported module function).
  4. Module imports ideally should be contained to the registered modules within state. In other words, when you import the module, it should just overwrite the module Component string with a Component function. That way, all of the relevant information is placed within one object.
  5. No need to mix and match template literals with string concatenation. Use one or the other.
  6. Use the setState callback to spread any previousState before overwriting it. Much simpler and cleaner looking.
  7. Wrap your import statement within a try/catch block, otherwise, if the module doesn't exist, it may break your application.

Working example (I'm just using React state for this simple example, I also didn't touch any of the other files, which can be simplified as well):

Edit wispy-thunder-jtc6c


App.js

import React from "react";
import Navigation from "./components/MainNavigation";
import Routes from "./routes";
import { plugins } from "./modules/manifest.json";
import "./assets/css/App.css";

class App extends React.Component {
  state = {
    importedModules: []
  };

  componentDidMount = () => {
    this.importPlugins();
  };

  importPlugins = () => {
    if (plugins) {
      try {
        const importedModules = [];
        const importPromises = plugins.map(plugin =>
          import(`./modules/${plugin.path}/${plugin.file}`).then(module => {
            importedModules.push({ ...plugin, Component: module.default });
          })
        );

        Promise.all(importPromises).then(() =>
          this.setState(prevState => ({
            ...prevState,
            importedModules
          }))
        );
      } catch (err) {
        console.error(err.toString());
      }
    }
  };

  render = () => (
    <div className="App">
      <Navigation />
      <Routes {...this.state} />
    </div>
  );
}

export default App;

routes/index.js

import React from "react";
import React from "react";
import isEmpty from "lodash/isEmpty";
import { Switch, Route } from "react-router-dom";
import ProjectForm from "../modules/core/forms/new-project-form";
import NewPostForm from "../modules/core/forms/new-post-form";
import ProjectLoop from "../modules/core/loops/project-loop";
import Home from "../home";

const Routes = ({ importedModules }) => (
  <Switch>
    <Route exact path="/" component={Home} />
    <Route exact path="/projectlist/:filter" component={ProjectLoop} />
    <Route exact path="/newproject/:type/:id" component={ProjectForm} />
    <Route exact path="/newpost/:type" component={NewPostForm} />
    {!isEmpty(importedModules) &&
      importedModules.map(({ path, Component }) => (
        <Route key={path} exact path={`/${path}`} component={Component} />
      ))}
  </Switch>
);

export default Routes;
like image 168
Matt Carlotta Avatar answered Sep 21 '22 17:09

Matt Carlotta