Dynamically import component in React based on config entries - reactjs

I have multiple apps as part of one React-redux-typescript project. All of these individual apps are part of a core-application. I want to dynamically set up routing.
My current routes look like this:
Routes.tsx
import HomePageApp from "../Components/Home/HomeApp";
import TestApp from "../Components/Test/TestApp";
export default function Routes() {
return (
<Switch>
<RedirectIfAuthenticated
exact={true}
isAuthenticated={true}
path={Path.homePath} --> "/"
component={HomePage} ---> AppName coming from import statement on top
redirectPath={Path.homePath} --> "/"
/>
<RedirectIfAuthenticated
isAuthenticated={true}
path={Path.apps.test} --> "/test"
component={TestApp} --> AppName from import on top
redirectPath={Path.homePath} --> "/"
/>
</Switch>
);
}
And RedirectIfAuthenticated simply redirects to correct applications' landing pages.
RedirectIfAuthenticated.tsx
export default function RedirectIfAuthenticated({
component,
redirectPath,
isAuthenticated,
...rest
}: IRedirectIfAuthenticatedProps) {
const Component = component;
const render = (renderProps: RouteComponentProps<any>) => {
let element = <Component {...renderProps} />;
return element;
};
return <Route {...rest} render={render}/>;
}
I've a config file like this:
Manifest.ts
export let manifest = {
apps: [
{
componentPath: "/Test/App",
path: "/test",
name: "Test"
},
...more objects for other apps
]
};
In my Routes.tsx, I want to make use of my manifest to render the RedirectIfAuthenticated component.
so I can figure out this change:
for brevity showing the dirty approach but the actual code iterates over the manifest using .map and renders RedirectIfAutenticated.
const app = manifest.apps.find(app => app.name === "Test");
<Switch>
<RedirectIfAuthenticated
isAuthenticated={true}
path={app.path} --> "/test"
component={What should I do here? How to pass component reference by path??}
redirectPath={"/"} ==> I typically get this from my manifest..
/>
</Switch>
One option is to do this:
component={require("path from manifest").default}
but our tslint throws a bunch of errors at this. Other than this I can't figure out how to pass the component reference here dynamically. Looking for a better approach.
The Routes.tsx needs to be dynamic so that adding new apps is a matter of configuration so I can't do imports on top because I dont know what's gonna be added in config. Thanks.

I was able to use dynamic imports to achieve this. I used this article to understand a few concepts.
private loadComponentFromPath(path: string) {
import(`../../ScriptsApp/${path}`).then(component =>
this.setState({
component: component.default
})
);
}
One important distinction here is if I don't give the path from the root of the app, I get an error saying "Unable to find the module". This is why I've given the full path from the root.

Related

Gatsby dynamic routes: 404 instead of component, can't configure

I made a simple site with Gatsby.js and can't configure dynamic routes.
I have index.js page (was automatically created by react), that looks like:
import * as React from 'react'
const IndexPage = () => {
return (
<Layout
pageTitle="Home Page"
>
Some text for my main page
</Layout>
)
}
export const Head = () => <title>Home Page</title>
export default IndexPage
Layout components includes Header that looks like:
import React, { useState, useEffect } from 'react';
import { Link } from 'gatsby';
const Header = () => {
return (
<Wrapper style={{ *some styles* }}>
<Link to="/">Index</Link>
<Link to="/projects">Projects</Link >
</Wrapper>
)
};
export default Header;
I have my Projects page that looks like this:
import * as React from 'react'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Layout from '../layout'
const Projects = () => {
return (
<BrowserRouter>
<Layout>
<Wrapper>
<Routes>
<Route path="projects/:projectID/" component={ProjectDetails} />
</Routes>
<MyProjectLink to="/projects/1">
Project 1
</MyProjectLink>
<MyProjectLink to="/projects/2">
Project 2
</MyProjectLink>
</Wrapper>
</Layout>
</BrowserRouter>
)
}
export const Head = () => <title>Our Projects</title>
export default Projects
And I have my ProjectDetails component:
import React from 'react';
import { useParams } from 'react-router';
import Layout from '../../pages/layout';
const ProjectDetails = () => {
const { projectID } = useParams();
return (
<Layout>
<Wrapper>
<h2>Project {projectID}</h2>
</Wrapper>
</Layout>
);
}
export default ProjectDetails;
The problem is that when I navigate to localhost:8000/projects/1 (or "2", or "3", or "100500") I see a 404 page instead of my ProjectDetails component.
I've tried wrapping the index page to BrowserRouter and move the routes with my route there, but that's a dumb idea in my opinion (and it doesnt work).
Did I miss something? Some features of Gatsby (v5) that I don't know about? I'm new to Getsby and to be honest I thought that dynamic routes here work the same way as in React Router.
Gatsby extends its routing from React, however, the way to create routes is slightly different.
As far as I understand your code, you are trying to create a template page for projects: this can be simply done by creating a file inside /templates folder. A simple component like this should work:
const Projects = ({ data }) => {
return (
<Layout>
<Wrapper></Wrapper>
</Layout>
);
};
export const Head = () => <title>Our Projects</title>
export default Projects
This template, as long as you use it when creating pages (using either gatsby-node.js or File System Route API) will be used to hold each specific project data.
Each project data will be queried using GraphQL and held inside props.data but without knowing your source (can be markdown, JSON, CMS, etc) I can't provide a sample query.
Once Gatsby infers its GraphQL nodes from your data source, you can use it to get all projects, a specific project, or any other GraphQL data you need on any page/template (page query) or even using static queries.
The idea should be similar to:
// gatsby-node.js
projects.forEach(({ node }, index) => {
createPage({
path: node.fields.slug,
component: path.resolve(`./src/templates/project.js`),
context: {
title: node.title,
},
})
})
In your gatsby-node.js (or File System Route API) you get all projects, loop through them and createPage for each project. The path (URL) for each project will be the slug field (node.fields.slug) but you can use whatever you want. Gatsby will create dynamic pages based on this field.
Then you decide which component will be used as a template: path.resolve(`./src/templates/project.js`) in this case and finally, you populate the context to add a unique value (title in this case: again, this can be an id, the slug, etc). This value will be used to filter the node in the template.
In your Project template:
export const query = graphql`
query ($title: String) {
mdx(title: {eq: $title}) {
id
title
}
}
`
In this case, I'm using markdown-based sources (that's why the mdx node) and this node is filtered by the title ($title) using the context value. The data will be inside props.data of the template. Again, if you want to fetch all projects you will have available an allMarkdown or allMdx (or allJSON...) depending on the source node)

Looping through route maps in React Main.js

I am using react-router-dom for my blogs.
My Main.js looks like
const Main = () => {
return (
<Switch> {/* The Switch decides which component to show based on the current URL.*/}
<Route exact path='/' component={Home}></Route>
<Route exact path='/1' component={art1}></Route>
<Route exact path='/2' component={art2}></Route>
{/* <Route exact path='/3' component={art3}></Route> */}
</Switch>
);
}
and I want to make it understand automatically as component = "art"+path. How should I go about it?
UPDATED ANSWER
Code has been further reduced by using dynamic imports with React.lazy. (See initial answer further below for original code)
import { lazy, Suspense } from "react";
import { Switch, Route } from "react-router-dom";
const Main = () => {
return (
<Switch> {/* The Switch decides which component to show based on the current URL.*/}
<Route exact path='/' component={Home}></Route>
<Suspense fallback="">
{
[...Array(3).keys()].map((i) => {
const artComp = lazy(() => import(`./components/Art${i+1}`));
return (<Route exact path={`/${i+1}`} key={i+1} component={artComp}></Route>);
})
}
</Suspense>
</Switch>
);
}
View updated demo on codesandbox
What the code does:
Create a Suspense component to wrap the Routes which components will be dynamically generated via React.lazy.
Inside the map function:
Generate component dynamically
Set the Route's component property to the above component
Explanation:
With React.lazy, you can dynamically import a module and render it into a regular component.
React.lazy takes a function that calls a dynamic import. This returns a promise which resolves to the imported module.
Note: the components in the imported modules should be default exports, otherwise the promise resolves to undefined.
Lazy loading means loading only what is currently needed. As the components will be lazy components, they'll be loaded only when first rendered (i.e. when you click on the corresponding link for the first time).
Notes:
No fallback has been specified for the Suspense in the answer.
With a Suspense, you can specify a fallback content (e.g. a loading indicator) to render while waiting for the lazy component to load.
The fallback content can be an HTML string, e.g.
fallback={<div>Loading...</div>}
or a component, e.g.:
fallback={<LoadingIndicator/>}
(21.08.21) React.lazy and Suspense are not yet available for server-side rendering. If you want to do this in a server-rendered app, React recommends Loadable Components.
For more information, view the React documentation about React.lazy.
INITIAL ANSWER
How about this?
import Art1 from '...';
import Art2 from '...';
import Art3 from '...';
// and so on
const Main = () => {
const artComponents = {
art1: Art1,
art2: Art2,
art3: Art3,
// and so on
};
return (
<Switch> {/* The Switch decides which component to show based on the current URL.*/}
<Route exact path='/' component={Home}></Route>
{
[...Array(3).keys()].map((i) => {
const artComp = artComponents[`art${i+1}`];
return (<Route exact path={`/${i+1}`} key={i+1} component={artComp}></Route>)
})
}
</Switch>
);
}
What this does:
Import "art"+path components into file
Note: component name should start with a capital letter.
Store imported components in an object
Create an empty array with 3 empty slots
Note: replace 3 by the number of "art"+path routes you need to generate.
Access the array's indexes
Create an array with those indexes
For each array index:
Compute the required "art"+path based on the index, and get the matching component from the object in step 2
Return a <Route>, with the path set based on the index and the component set to the matching component above
E.g. for index 0:
artComp will be artComponents[1] a.k.a. Art1
path will be /1
component will be Art1
import { useRouteMatch } from 'react-router-dom';
let { path} = useRouteMatch();
after getting the path remove the "/" from the path in the component concatenation
<Route exact path={path} component={art${path.substring(1)}}>
What you want to do isn't possible because you cannot create a variable name from a string in Javascript. I assume that you have something like
art1 = () => {
// return some JSX
}
and
art2 = () => {
// return some JSX
}
When you have variable names like this that only differ in a number, then you are almost certainly doing something wrong.

Gatsby wildcard route

Is it not possible to route to the same component with a wildcard path?
If in React I have something like:
<Router>
<Switch>
<Route path="/path/:id" children={<Component />} />
</Switch>
</Router>
all the requests:
/path/123
/path/123/p
/path/123/p/1
will route to the same /path/123
How can I tell Gatsby to do the same?
createPage({
path: `/path/123/*`,
component,
context
})
Or what is the solution to this problem, a redirect engine of some sorts?
I think you are looking for client-only routes. Given a page (or template if it's created from gatsby-node.js) you can:
import React from "react"
import { Router } from "#reach/router"
import Layout from "../components/Layout"
import SomeComponent from "../components/SomeComponent"
const App = () => {
return (
<Layout>
<Router basepath="/app">
<SomeComponent path="/path" />
</Router>
</Layout>
)
}
export default App
Note: assuming a src/pages/app/[...].js page (File System Route API structure).
When a page loads, Reach Router looks at the path prop of each component nested under <Router />, and chooses one to render that best matches window.location (you can learn more about how routing works from the #reach/router documentation).
Alternatively, you can use an automated approach (plugin: gatsby-plugin-create-client-paths) by:
{
resolve: `gatsby-plugin-create-client-paths`,
options: { prefixes: [`/path/*`] },
},
Which will validate all routes under /path.
Or for a more customizable approach, in your gatsby-node.js:
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/path/)) {
page.matchPath = "/path/*"
// Update the page.
createPage(page)
}
}
Disclaimer: These routes will exist on the client only and will not correspond to index.html files in an app’s built assets. If you’d like site users to be able to visit client routes directly, you’ll need to set up your server to handle those routes appropriately.

Building portfolio website and I want to include my github link [duplicate]

Since I'm using React Router to handle my routes in a React app, I'm curious if there is a way to redirect to an external resource.
Say someone hits:
example.com/privacy-policy
I would like it to redirect to:
example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies
I'm finding exactly zero help in avoiding writing it in plain JavaScript at my index.html loading with something like:
if (window.location.path === "privacy-policy"){
window.location = "example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies"
}
Here's a one-liner for using React Router to redirect to an external link:
<Route path='/privacy-policy' component={() => {
window.location.href = 'https://example.com/1234';
return null;
}}/>
It uses the React pure component concept to reduce the component's code to a single function that, instead of rendering anything, redirects browser to an external URL.
It works both on React Router 3 and 4.
With Link component of react-router you can do that. In the "to" prop you can specify 3 types of data:
a string: A string representation of the Link location, created by concatenating the location’s pathname, search, and hash properties.
an object: An object that can have any of the following properties:
pathname: A string representing the path to link to.
search: A string representation of query parameters.
hash: A hash to put in the URL, e.g. #a-hash.
state: State to persist to the location.
a function: A function to which current location is passed as an argument and which should return location representation as a string or as an object
For your example (external link):
https://example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies
You can do the following:
<Link to={{ pathname: "https://example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies" }} target="_blank" />
You can also pass props you’d like to be on the such as a title, id, className, etc.
There isn’t any need to use the <Link /> component from React Router.
If you want to go to external link use an anchor tag.
<a target="_blank" href="https://meetflo.zendesk.com/hc/en-us/articles/230425728-Privacy-Policies">Policies</a>
It doesn't need to request React Router. This action can be done natively and it is provided by the browser.
Just use window.location.
With React Hooks
const RedirectPage = () => {
React.useEffect(() => {
window.location.replace('https://www.google.com')
}, [])
}
With React Class Component
class RedirectPage extends React.Component {
componentDidMount(){
window.location.replace('https://www.google.com')
}
}
Also, if you want to open it in a new tab:
window.open('https://www.google.com', '_blank');
I actually ended up building my own Component, <Redirect>.
It takes information from the react-router element, so I can keep it in my routes. Such as:
<Route
path="/privacy-policy"
component={ Redirect }
loc="https://meetflo.zendesk.com/hc/en-us/articles/230425728-Privacy-Policies"
/>
Here is my component in case anyone is curious:
import React, { Component } from "react";
export class Redirect extends Component {
constructor( props ){
super();
this.state = { ...props };
}
componentWillMount(){
window.location = this.state.route.loc;
}
render(){
return (<section>Redirecting...</section>);
}
}
export default Redirect;
Note: This is with react-router: 3.0.5, it is not so simple in 4.x
I went through the same issue. I want my portfolio to redirect to social media handles. Earlier I used {Link} from "react-router-dom". That was redirecting to the sub directory as here,
Link can be used for routing web pages within a website. If we want to redirect to an external link then we should use an anchor tag. Like this,
Using some of the information here, I came up with the following component which you can use within your route declarations. It's compatible with React Router v4.
It's using TypeScript, but it should be fairly straightforward to convert to native JavaScript:
interface Props {
exact?: boolean;
link: string;
path: string;
sensitive?: boolean;
strict?: boolean;
}
const ExternalRedirect: React.FC<Props> = (props: Props) => {
const { link, ...routeProps } = props;
return (
<Route
{...routeProps}
render={() => {
window.location.replace(props.link);
return null;
}}
/>
);
};
And use with:
<ExternalRedirect
exact={true}
path={'/privacy-policy'}
link={'https://example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies'}
/>
The simplest solution is to use a render function and change the window.location.
<Route path="/goToGoogle"
render={() => window.location = "https://www.google.com"} />
If you want a small reusable component, you can just extract it like this:
const ExternalRedirect = ({ to, ...routeProps }) => {
return <Route {...routeProps} render={() => window.location = to} />;
};
and then use it (e.g. in your router switch) like this:
<Switch>
...
<ExternalRedirect exact path="/goToGoogle" to="https://www.google.com" />
</Switch>
I had luck with this:
<Route
path="/example"
component={() => {
global.window && (global.window.location.href = 'https://example.com');
return null;
}}
/>
I solved this on my own (in my web application) by adding an anchor tag and not using anything from React Router, just a plain anchor tag with a link as you can see in the picture screenshot of using anchor tag in a React app without using React Router
Basically, you are not routing your user to another page inside your app, so you must not use the internal router, but use a normal anchor.
Although this is for a non-react-native solution, but you can try.
In React Router v6, component is unavailable. Instead, now it supports element. Make a component redirecting to the external site and add it as shown.
import * as React from 'react';
import { Routes, Route } from "react-router-dom";
function App() {
return(
<Routes>
// Redirect
<Route path="/external-link" element={<External />} />
</Routes>
);
}
function External() {
window.location.href = 'https://google.com';
return null;
}
export default App;
In React Route V6 render props were removed. It should be a redirect component.
RedirectUrl:
const RedirectUrl = ({ url }) => {
useEffect(() => {
window.location.href = url;
}, [url]);
return <h5>Redirecting...</h5>;
};
Route:
<Routes>
<Route path="/redirect" element={<RedirectUrl url="https://google.com" />} />
</Routes>
I think the best solution is to just use a plain old <a> tag. Everything else seems convoluted. React Router is designed for navigation within single page applications, so using it for anything else doesn't make a whole lot of sense. Making an entire component for something that is already built into the <a> tag seems... silly?
To expand on Alan's answer, you can create a <Route/> that redirects all <Link/>'s with "to" attributes containing 'http:' or 'https:' to the correct external resource.
Below is a working example of this which can be placed directly into your <Router>.
<Route path={['/http:', '/https:']} component={props => {
window.location.replace(props.location.pathname.substr(1)) // substr(1) removes the preceding '/'
return null
}}/>
I don't think React Router provides this support. The documentation mentions
A < Redirect > sets up a redirect to another route in your application to maintain old URLs.
You could try using something like React-Redirect instead.
I was facing the same issue and solved it using by http:// or https:// in React.
Like as:
<a target="_blank" href="http://www.example.com/" title="example">See detail</a>
You can use for your dynamic URL:
<Link to={{pathname:`${link}`}}>View</Link>
For V3, although it may work for V4. Going off of Eric's answer, I needed to do a little more, like handle local development where 'http' is not present on the URL. I'm also redirecting to another application on the same server.
Added to the router file:
import RedirectOnServer from './components/RedirectOnServer';
<Route path="/somelocalpath"
component={RedirectOnServer}
target="/someexternaltargetstring like cnn.com"
/>
And the Component:
import React, { Component } from "react";
export class RedirectOnServer extends Component {
constructor(props) {
super();
// If the prefix is http or https, we add nothing
let prefix = window.location.host.startsWith("http") ? "" : "http://";
// Using host here, as I'm redirecting to another location on the same host
this.target = prefix + window.location.host + props.route.target;
}
componentDidMount() {
window.location.replace(this.target);
}
render(){
return (
<div>
<br />
<span>Redirecting to {this.target}</span>
</div>
);
}
}
export default RedirectOnServer;
I am offering an answer relevant to React Router v6 to handle dynamic routing.
I created a generic component called redirect:
export default function Redirect(params) {
window.location.replace('<Destination URL>' + "/." params.destination);
return (
<div />
)
}
I then called it in my router file:
<Route path='/wheretogo' element={<Redirect destination="wheretogo"/>}/>
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
function App() {
return (
<Router>
<Route path="/" exact>
{window.location.replace("http://agrosys.in")}
</Route>
</Router>
);
}
export default App;
Using React with TypeScript, you get an error as the function must return a React element, not void. So I did it this way using the Route render method (and using React router v4):
redirectToHomePage = (): null => {
window.location.reload();
return null;
};
<Route exact path={'/'} render={this.redirectToHomePage} />
Where you could instead also use window.location.assign(), window.location.replace(), etc.
Complementing Víctor Daniel's answer here: Link's pathname will actually take you to an external link only when there's the 'https://' or 'http://' before the link.
You can do the following:
<Link to={{ pathname:
> "https://example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies"
> }} target="_blank" />
Or if your URL doesn't come with 'https://', I'd do something like:
<Link to={{pathname:`https://${link}`}} target="_blank" />
Otherwise it will prepend the current base path, as Lorenzo Demattécommented.
If you are using server-side rending, you can use StaticRouter. With your context as props and then adding <Redirect path="/somewhere" /> component in your app. The idea is every time React Router matches a redirect component it will add something into the context you passed into the static router to let you know your path matches a redirect component.
Now that you know you hit a redirect you just need to check if that’s the redirect you are looking for. then just redirect through the server. ctx.redirect('https://example/com').
You can now link to an external site using React Link by providing an object to to with the pathname key:
<Link to={ { pathname: '//example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies' } } >
If you find that you need to use JavaScript to generate the link in a callback, you can use window.location.replace() or window.location.assign().
Over using window.location.replace(), as other good answers suggest, try using window.location.assign().
window.location.replace() will replace the location history without preserving the current page.
window.location.assign() will transition to the URL specified, but will save the previous page in the browser history, allowing proper back-button functionality.
location.replace()
location.assign()
Also, if you are using a window.location = url method as mentioned in other answers, I highly suggest switching to window.location.href = url.
There is a heavy argument about it, where many users seem to adamantly want to revert the newer object type window.location to its original implementation as string merely because they can (and they egregiously attack anyone who says otherwise), but you could theoretically interrupt other library functionality accessing the window.location object.
Check out this conversation. It's terrible.
JavaScript: Setting location.href versus location
I was able to achieve a redirect in react-router-dom using the following
<Route exact path="/" component={() => <Redirect to={{ pathname: '/YourRoute' }} />} />
For my case, I was looking for a way to redirect users whenever they visit the root URL http://myapp.com to somewhere else within the app http://myapp.com/newplace. so the above helped.

How do I go to a specific page in React based on a URL param

As an example when entering http://localhost:3000/ui/?goto=/ui/entity/e2 in the browser I'd like to go to the Entity component e2.
This is my router:
<Route path="/ui/" component={App}>
<IndexRoute component={EntitiesPage} />
<Route component={Entity} path="entity/:id" />
<Route component={NotFound} path="*" />
</Route>
This is the App component:
import React from 'react'
const App = React.createClass( {
render() {
let gotoUrl = this.props.location.query.goto;
if (gotoUrl) {
// go here: gotoUrl;
} else {
return (
<div className="App">
{this.props.children}
</div>
)
}
}
})
export default App
this.context is empty.
this.props has:
history
location
route
routeParams (empty)
routes
UPDATE:
I've ended up using this:
import React from 'react'
import { withRouter } from 'react-router'
const App = React.createClass( {
componentWillMount() {
let gotoUrl = this.props.location.query.goto;
if (gotoUrl) {
this.props.router.replace(gotoUrl);
}
},
render() {
return (
<div className="App">
{this.props.children}
</div>
);
}
})
export default withRouter(App)
One thing that might be tripping you up is that render should have no side effects.
A "side effect" is anything that changes what's going on in your app*: updating state, making AJAX calls, or in this case, altering the page location. The render method should only read from the current state of the component, then return a value.
Because you're already using React.createClass, the best way to handle this is by adding a separate method that React handles specially: componentWillMount. I'd recommend you put your "redirect" logic here.
In order to properly change the page location, you'll need access to the browser history object which react-router manipulates. You can import this from the react-router library itself and directly call methods on it:
// At top of file
import { browserHistory } from 'react-router'
// Then, in your component:
componentWillMount() {
let gotoUrl = this.props.location.query.goto;
if (gotoUrl) {
// NOTE: this may have security implications; see below
browserHistory.push(gotoUrl);
}
}
Source: documentation.
I'd suggest that, instead of using query.goto, you instead select a parameter that can be easily validated, such as the entity ID itself (a simple regex can make sure it's valid). Otherwise, an unscrupulous user might send a link to another user and cause them to access a page that they didn't mean to.
*Note: there are stricter definitions of "side effect" out there, but this one is pretty useful for React development.
You should use browserHistory
import { browserHistory } from 'react-router';
...
if (gotoUrl) {
browserHistory.push(gotoUrl)
}
Let me know if this works

Resources