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
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
Okey pokey. There are a lot of moving parts here that can be vastly simplified.
Route
(in your case, you were trying to map the module's registered string name to the route, instead of the imported module function).previousState
before overwriting it. Much simpler and cleaner looking.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):
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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With