Dynamically inject data in React Router Routes - javascript

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

Okey pokey. There are a lot of moving parts here that can be vastly simplified.
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.
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).
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).
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.
No need to mix and match template literals with string concatenation. Use one or the other.
Use the setState callback to spread any previousState before overwriting it. Much simpler and cleaner looking.
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):
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;

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

I think the problem is the way you are trying to render <item.component /> but not sure, did you get the same error whit this?
Try:
<Route exact path={`/${item.path}`} render={function() {
return React.createElement(item.component, props)
}}></Route>
)}

Related

React Suspense lazy loading without fallback

I want to lazy load my components to decrease my initial bundle size and get components on the fly using code splitting using react router.
However, when using React Suspense, they force you to use a fallback for loading.
This wouldn't work:
const lazyLoadComponent = Component =>
props => (
<Suspense> // Missing fallback property
<Component {...props} />
</Suspense>
);
In my case I am rendering html from the server so I don't want to use a spinner.
This would create a useless flicker on my screen!
I.e.:
Html loads
Place holder appears
PageComponent for the route gets loaded
I have my own spinner that loads a feed from within the page component
In my case the html corresponds to the react component that gets loaded.
Is there any known hack to easily work around this problem (except for creating a loader for any route that copies the html (!!), which by the way, would make lazy loading useless).
I am a bit displeased with "forcing" us to add a loader and I don't understand the logic behind the decision to make it mandatory.
Try to use code splitting in the docs
fallback props is just a React element, you can set it for null.
const MyLazyComponent= React.lazy(() => import('./MyComponent'));
<Suspense fallback={null}>
<MyLazyComponent />
</Suspense>
I created an issue for this on Github: https://github.com/facebook/react/issues/19715
There isn't a current clean solution using React-Router / React.
This is however foreseen in a future release using concurrent mode. As mentioned by Dan Abramov:
Regarding your concrete feature request, I think I can reframe it
slightly differently. It's not that you want "optional fallback" since
that wouldn't make sense for new screens (we've got to show
something). What I believe you're looking for is a way to skip showing
the fallback if the content is already in HTML. This is precisely how
React behaves in Concurrent Mode so the feature request is already
implemented (and will eventually become the default behavior in a
stable release).
For me it is not a problem to wait, so currently I will omit lazy-loading the routes as this concerns a hobby-project and I have time to wait for a future release.
In my experience (with React 17), there's no flickering happens if you pass null to fallback param.
I have a Modal component that renders lazy components.
Here's my Typescript solution:
type LazyLoadHOC = {
component: React.LazyExoticComponent<any>,
fallback?: React.ComponentType | null,
[x:string]: any
};
export const LazyLoad: React.FC<LazyLoadHOC> = ({
component: Component, fallback = null, ...props
}) => {
return (
<React.Suspense fallback={fallback}>
<Component {...props} />
</React.Suspense>
);
};
Here's my Modal:
const AddressFormModel = React.lazy(() => import('#components/address/address-form-modal'));
<Modal show={isOpen} backdrop={'static'} dialogClassName='custom-modal'>
<ModalBody>
{view === 'ADDRESS-FORM' && <LazyLoad component={AddressFormModel} />}
</ModalBody>
</Modal>
This will ensure to not trigger your global React.Suspense.
So basically all we have to do is, wrap the loader in a component and load the script using that component (Loadable here).
This way we can use React.lazy wihtout any animation.
Since our fallback is empty you wont see any animation and your content will be visible always. There will not be any flicker of content hiding.
loadable.js
import React, { Suspense } from 'react';
const Loadable = (Component) => (props) => (
<Suspense fallback={<></>}>
<Component {...props} />
</Suspense>
);
export default Loadable;
We can wrap the import in Loadable so that it will load lazy.
route.js
import React, { lazy } from 'react';
import Loadable from './components/Loadable';
// page import
const Home = Loadable(lazy(() => import('./pages/home')));
// define routes
const routes = [
{
path: "dashboard",
element: <Layout />,
children: [
{
path: "",
element: <Home />,
},
]
},
]

How to create website endpoints based on search results?

I'm building a ReactJS website as part of a web dev bootcamp project.
I made a search feature using flask routes between the reactjs endpoints (../Language.js) and my Sqlite3 database.
http://localhost:3000/kanjisearch
How do I make the result of a search into an endpoint itself though? For example if a user searches for "german verbs" the browser displays something along the lines of:
http://localhost:3000/kanjisearch?=german+verbs
I want this so that when users hit the forward or back arrows on the browser, it takes the user to the previous search, NOT the previous page they were on.
Can I do this is react/javascript? Something else?
Thank you.
yes you can, you can use react-router-dom library
just do yarn add react-router-dom and create a route.js file and do the following
import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import someScreen from "../somewhere" // this would be your Screen that you wanna show
const Routes = () => {
return (
<Router>
<Switch>
<Route path="/container/language/:language" component={someScreen} />
</Switch>
</Router>
);
};
export default Routes;
after that you have to fix your app.js file as the following
import React from 'react';
import './App.css';
import Routes from './router '; //the location of your router
function App() {
return (
<div className="App">
<header className="App-header">
<Routes/>
</header>
</div>
);
}
export default App;
go to your component and do export default withRouter(someScreen)
In your screen Since you have now a connected Class (to the routing) you can access match inside your props, this props is created by react-router and contains informations about what matched in this route : For exemple match.params.language would contains the :language from the route, meaning if u have /german+verbs
and now you can use the url like http://localhost:3000/container/language/german+verbs and then german+verbs
your parameters would be passed to your component as as
match.params = {
language: 'language+verbs'
}
use this is in your compDidMount method
Well... what language do you want to use? :-) . You can do it in any of those you mentioned.
Since you mentioned ReactJS first, you can do it in javascript by using the window.location object. Just set and read the hash. BTW the hash can be anything and is ignored by the browser, but your JS can look at it. Your url would look something like this:
http://localhost:3000/container/language/Language#search=german+verbs.

Export `react-router` Redirect from shared component library

I have a shared (React) component library that I'm building. There is a PrivateRoute component that I am wanting to include. However, when I import the component from the module library into another application, I get an error:
Error: Invariant failed: You should not use <Redirect> outside a <Router>
The PrivateRoute component wraps the react-router/Route component with authentication logic and redirects unauthenticated requests to login:
component-library
import { Route, Redirect } from 'react-router';
/* ... */
class PrivateRoute extends Component {
/* ... */
render() {
const {
component: Comp, authState, loginPath, ...rest
} = this.props;
return (
<Route
{...rest}
render={props => authState === SIGNED_IN ? (
<Comp {...props} />
) : (
<Redirect
to={{
pathname: loginPath,
}}
/>
)}
/>
);
}
}
I then import the component into a separate (React) project:
create-react-app
import { Router } from 'react-router';
import { PrivateRoute } from 'component-library';
/* ... */
class App extends Component {
// "history" is passed in via props from the micro frontend controller.
/* ... */
render() {
return (
<Router history={this.props.history}>
{/* ... */}
<PrivateRoute path="/protected" component={ProtectedView} />
</Router>
);
}
}
This will work as expected if the PrivateRoute component is defined in the create-react-app application. However, moving this component to the shared library results in the error.
I have tried building the library with webpack output libraryTarget set to commonjs2. But, I've also tried umd. I've also tried with Rollup. All with the same results.
webpack.config.js
module.exports = {
//...
output: {
path: path.resolve(__dirname, 'dist/'),
publicPath: '',
filename: '[name].js',
libraryTarget: 'commonjs2',
},
//...
};
My assumption is that the issue is with building the component library as the Invariant error is thrown when Redirect is unable to find the RouterContext. Although the library builds without errors, it seems that importing compiled/built code is a problem.
Could also be two instances of React causing an issue with the Context API. However, react-router is not using the Context API. It's using the mini-create-react-context polyfill.
Any thoughts or ideas on how to resolve this?
You have to import router (assuming you're using V4) from react-router-dom, eg:
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
In v4, react-router exports the core components and functions.
react-router-dom exports DOM-aware components, like <Link> (which
renders an <a>) and <BrowserRouter> (which interacts with the
browser's window.history ).
react-router-dom re-exports all of react-router's exports, so you only
need to import from react-router-dom in your project.
Ref: https://github.com/ReactTraining/react-router/issues/4648#issuecomment-284479720
I did finally discover the issue which had little to do with react-router and more with React. I found that this error would only show in local development because the component-library was linked via npm link.
The resolution came from this answer: https://stackoverflow.com/a/38818358/715597
The solution in my case was to link React and React Router in the component library to the applications reference of React and React Router:
# link the component library
cd my-app
npm link ../component-library
# link its copy of React back to the app's React
cd ../component-library
npm link ../my-app/node_modules/react
npm link ../my-app/node_modules/react-router
Can't you do it using a function
if(authState === SIGNED_IN){
return <Route
{...rest}
render={<Comp {...props} />
/>
}else{
// here you can use window.location
// or
// render a different component (which shows unauthorized or something you want)
}
you only needed to remove dependency "react-router-dom": "5.0.0" from package.json to fix the error.

How to make a single React component then fill it with information based on URL?

I am building a list of people my company is working with. Since it is constantly updating with new people I decided to make a single page web app purely in React as a way to learn it since I am new to it and I wanted to learn it for sometime now.
I have index.js and people.js. In people.js I made an object for every person and they each have own atributes (age, location etc.).
Since I don't know how to do it properly I made for each person Home and Single component of it.
Home component is something like:
export class AnnHome extends Component{
render(){
return(
<div>
<Link to={{pathname: 'influencer/Ann',
component: 'AnnSingle'
}}>
<img
src={require('./img/ann.png')}
alt="{Kofs.name}"
className="Avatar rounded-circle"
/></Link>
<p>{Ann.name}</p>
</div>
)
}
}
Before that component I have defined object 'Ann' with it's info.
My question is:
How to make a one Home component and one Single Component like a template so when I go to /ann to fill SingleComponent with Ann info.
Home Component would be like a list of all clients (people):
https://www.w3schools.com/howto/howto_css_team.asp
something like this.
I currently have a lot of Home components that I've put manually.
Hopefully I described my problem, my english is rusty :D
Hopefully this can help get you started.
As I mentioned above, you want to use the React Router to listen to route changes. Then one specifies which component should be mounted for each route. Here I use the route to match on the individual's name (e.g. so /stace matches the element in people where the person's name attribute is Stace.
import React, { Component } from 'react';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';
const people = [
{name: 'Stace', img: 'https://placeimg.com/200/200/people'},
{name: 'Marlo', img: 'https://placeimg.com/201/200/people'},
]
const App = props => (
<BrowserRouter>
<div>
<Switch>
<Route path='/stace' component={Person} />
<Route path='/marlo' component={Person} />
</Switch>
{people.map((p, idx) => <Link to={'/' + p.name}>{p.name}</Link>)}
</div>
</BrowserRouter>
)
const Person = props => {
const name = props['match']['path'].toLowerCase().substring(1);
const person = people.filter(p => p.name.toLowerCase() == name)[0]
return (
<div className='person'>
<img src={person.img || ''}></img>
<h2>{person.name || ''}</h2>
</div>
)
}
export default App;
This is of course only meant to demonstrate the router-related concepts. If you use create-react-app, run yarn add react-router#4.3.0 && yarn add react-router-dom#4.3.1, and replace App.js with the content above, you'll be able to preview the app and explore the ideas expressed above. Clicking on a name will change the route to that name and display the data for that name. Below I've clicked on Stace:

How to test a React component that has Router, Redux and two HOCs... with Jest and Enzyme?

I am currently unable to find a solution to this problem.
I have a React Component that is connected to React Router 4, Redux store and is wrapped by two HOCs. Its pretty crazy, but this is how it was coded.
Here is the export to give you an idea:
export default withFetch(Component)(fetchData, mapStateToProps)
I am trying to run some basic tests on it:
it('should render self and subcomponents', () => {
const wrapper = shallow(<Component {...props} />)
expect(toJson(wrapper)).toMatchSnapshot()
})
Which outputs a console.log/snapshot of:
<Route render={[Function: render]} />
Things tried but no succeed:
I've tried wrapping my component in the Memory Router
Supply a redux store to the component
Used .dive() and .chilndren() to try and see the children
Tried mount and render with no success.
Still keeps rendering the <Route render={[Function: render]} />
Trying out :
<MemoryRouter>
<Component {...props} />
</MemoryRouter>
Still produces the same result.
Note that I've also tried importing my component as
import { Component } from './components/'
But it returns undefined.
Any help is deeply appreciated. Thank you! 😊🙏
I assume that by <Router> you are referring to BrowserRouter.
The best way is to isolate the wrapped component and test it with testing alternatives.
For example assume that you want to test that:
// App.jsx
export const App = () =>
<Router>
<ReduxProvider>
<AppInner>
</ReduxProvider>
</Router>
My suggestion is to test AppInner with testing env of Router & ReduxProvider.
In tests:
// AppInner.test.jsx
import {mount} from 'enzyme';
import {MemoryRouter} from 'react-router';
describe('AppInner', () => {
it('should do something', () => {
const TestingComponent = () =>
<MemoryRouter>
<ReduxProvider>
<AppInner />
<ReduxProvider>
<MemoryRouter>;
const component = mount(TestingComponent);
});
})
Pay attention that I've wrapped the AppInner with MemoryRouter, it allows your mimic router but without the dependency of the browser.
For more info you can read the testing section of react-router;

Categories