I am using react-loadable to split my code. I have opted to do this at route level to begin with. My routes.js looks like this:
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Loadable from 'react-loadable';
const Loading = props => {
if (props.error) {
return <div>Error loading page!</div>;
} else {
return null;
}
};
const AsyncHomepage = Loadable({ loader: () => import('../Scenes/Homepage/Homepage' /* webpackChunkName: "homepage" */), loading: Loading });
const AsyncSearchPage = Loadable({ loader: () => import('../Scenes/Search/Search' /* webpackChunkName: "search" */), loading: Loading });
const AsyncAboutUsPage = Loadable({ loader: () => import('../Scenes/AboutUs/AboutUs' /* webpackChunkName: "aboutus" */), loading: Loading });
// omitted for brevity
export const Routes = () => {
return (
<Switch>
<Route exact path="/" component={AsyncHomepage} />
<Route exact path="/search" component={AsyncSearchPage} />
<Route exact path="/about-us" component={AsyncAboutUsPage} />
// omitted for brevity
</Switch>
);
};
This is the only place I'm using react-loadable.
When I build the application, I get my named chunks as expected but I also get a bunch of random numbered chunks as well:
These other chunks seem to contain things like moment.js, react-select, there's another that contains react-dom which surely would just be included in every page?
What I'd like to know is why is this happening and how can I name these chunks or merge them with the page chunks?
Related
I'm trying to build a dynamic router with React. The idea is that the Routes are created from the data (object) received from the backend:
Menu Object:
items: [
{
name: "dashboard",
icon: "dashboard",
placeholder: "Dashboard",
path: "/",
page: "Dashboard",
exact: true,
},
{
name: "suppliers",
icon: "suppliers",
placeholder: "Suppliers",
path: "/suppliers",
page: "Suppliers",
exact: true,
}
]
The routes hook :
export const RouteHook = () => {
// Dispatch to fetch the menu object from the backend
const dispatch = useDispatch();
dispatch(setMenu());
// Select the menu and items from Redux
const { items } = useSelector(getMainMenu);
// useState to set the routes inside
const [menuItems, setMenuItems] = useState([]);
const { pathname } = useLocation();
useEffect(() => {
// Loop trough the menu Items
for (const item of items) {
const { page, path, name, exact } = item;
// Import dynamically the route components
import(`../pages/${name}/${page}`).then((result) => {
// Set the routes inside the useState
setMenuItems(
<Route exact={exact} path={path} component={result[page]} />
);
});
}
// Check if pathname has changed to update the useEffect
}, [pathname]);
return (
<Switch>
{/* Set the routes inside the switch */}
{menuItems}
</Switch>
);
};
Now here's the problem. Not all the components load. Usually the last component loads and when clicking on a diffrent route the component won't change. Except if you got to the page and refresh (F5).
What am I missing here? Is it possible to create full dynamic routes & components in react?
I'm not sure 100% what's going on here, but here's a problem I see:
const [menuItems, setMenuItems] = useState([]);
You're saying that menuItems is an array of something. But then:
import(`../pages/${name}/${page}`).then((result) => {
// Set the routes inside the useState
setMenuItems(
<Route exact={exact} path={path} component={result[page]} />
);
});
On every iteration you are setting the menu items to be a singular Route component. Probably what you're thinking youre doing is
const routes = items.map(item => {
const { page, path, name, exact } = item;
return import(`../pages/${name}/${page}`).then((result) => {
<Route exact={exact} path={path} component={result[page]} />
});
})
setMenuItems(routes)
But this makes no sense, because your map statement is returning a Promise.then function. I'm not entirely sure why you're dynamically importing the components here. You're better off doing a simple route mapping:
const routes = items.map(item => {
const { page, path, name, exact } = item;
return <Route exact={exact} path={path} component={components[page]} />
})
setMenuItems(routes)
Where components is an object whose keys are the values of page and whose values are actual components, i.e.:
const components = {
Suppliers: RenderSuppliers,
Dashboard: RenderDashboard
}
If you want these components lazy-loaded, use react suspense:
const Suppliers = React.lazy(() => import("./Suppliers"))
const Dashboard = React.lazy(() => import("./Dashboard"))
const components = {
Suppliers,
Dashboard,
}
const routes = items.map(item => {
const { page, path, name, exact } = item;
return (
<Suspense fallback={<SomeFallbackComponent />}>
<Route
exact={exact}
path={path}
component={components[page]}
/>
</Suspense>
)
})
setMenuItems(routes)
This is just a quick review of what may be going wrong with your code, without a reproducible example, its hard to say exactly.
Seth has some great suggestions, but here's how you can clean this up while still using dynamic imports.
Hopefully you can see that you are calling setMenuItems with a single Route component instead of all of them. Each time that you setMenuItems you are overriding the previous result and that's why only the last Route actually works -- it's the only one that exists!
Your useEffect depends on the pathname which seems like you are trying to do the routing yourself. Since you are using react-router-dom you would include all of the Route components in your Switch and let the router handle the routing.
So you don't actually need any state here.
You can use the React.lazy component import helper inside of the Route. You need a Suspense provider around the whole block in order to use lazy imports.
I don't like that you use two variables in the path for a component ../pages/${name}/${page}. Why not export the component from the ./index.js of the folder?
export const Routes = () => {
// Dispatch to fetch the menu object from the backend
const dispatch = useDispatch();
dispatch(setMenu());
// Select the menu and items from Redux
const items = useSelector((state) => state.routes.items);
return (
<Suspense fallback={() => <div>Loading...</div>}>
<Switch>
{items.map(({ exact, path, name }) => (
<Route
key={name}
exact={exact}
path={path}
component={React.lazy(() => import(`../pages/${name}`))}
/>
))}
</Switch>
</Suspense>
);
};
It works!
Code Sandbox Link
I'm using Gatsby and I would like to create a one site using multilanguage, so far I've defined pages/index.js which contains this:
import React from "react"
import Layout from "../components/layout/layout"
import BGTState from "../context/bgt/bgtState"
import { Router } from "#reach/router"
import Home from "../components/pages/home"
import Collection from "../components/pages/collection"
import NotFound from "../components/pages/404"
const IndexPage = () => {
return (
<BGTState>
<Layout>
<Router>
<Home path="/" />
<Collection path="collection/:id" />
<NotFound default />
</Router>
</Layout>
</BGTState>
)
}
export default IndexPage
and I have modified gatsby-node.js as:
// Implement the Gatsby API onCreatePage. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path === "/") {
page.matchPath = "/*"
createPage(page)
}
}
each request is forwarded on index.js, but there is a problem. I'm using the plugin gatsby-plugin-intl that add to the url a dynamic prefix like: http://localhost:3001/en/
If I visit http://localhost:3001/en/, then I get the NotFound component displayed because no Route match the url. Is there a way to prefix the url and reroute everything to the correct component?
Why you are using client-only routes/wrapping everything inside the <Router>?
I don't know what's the goal in your scenario to change the gatsby-node.js with:
// Implement the Gatsby API onCreatePage. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path === "/") {
page.matchPath = "/*"
createPage(page)
}
}
If you are not using client-only routes, you can remove them.
It's a broad question but, just define your languages and translation files. In your gatsby-config.js:
plugins: [
{
resolve: `gatsby-plugin-intl`,
options: {
// language JSON resource path
path: `${__dirname}/src/intl`,
// supported language
languages: [`en`,`es`],
// language file path
defaultLanguage: `en`,
// option to redirect to `/en` when connecting `/`
redirect: true,
},
},
]
The useIntl hook will capture the internal requests so, you just need to worry about the views, forgetting the routing:
import React from "react"
import { useIntl, Link, FormattedMessage } from "gatsby-plugin-intl"
const IndexPage = () => {
const intl = useIntl()
return (
<Layout>
<SEO title={intl.formatMessage({ id: "title" })}/>
<Link to="/page-2/">
<FormattedMessage id="go_page2" />
</Link>
</Layout>
)
}
export default IndexPage
Your Collection component should be a page, wrapped inside /page folder, or a custom collection with a specific id. If that page is dynamically created, you should manage the customizations in your gatsby-node.js, and, in that case, it should be a template of collections in that scenario.
To link between pages, I would recommend using page-queries to get the needed data to create your components. Your page should look like:
const IndexPage = () => {
return (
<BGTState>
<Layout>
<Link to="/"> // home path
<Link to="collection/1">
</Layout>
</BGTState>
)
}
export default IndexPage
The 404-page will automatically be handled by Gatsby, redirecting all wrong requests (in development will show a list of pages). Your other routing should be managed using the built-in <Link> component (extended from #reach/router from React).
To make dynamic the <Link to="collection/1"> link, you should make a page query, as I said, to get the proper link to build a custom dynamic <Link> from your data.
Once you installed the gatsby-plugin-intl plugin, all your pages will be prefixed automatically, however, to point to them using <Link> or navigate you need to get the current language and prefix it:
export const YourComponent = props => {
const { locale } = useIntl(); // here you are getting the current language
return <Link to={`${locale}/your/path`}>Your Link</Link>;
};
Because useIntl() is a custom hook provided by the plugin, the value of locale will be automatically set as you change the language.
How would I be able to test the router in the code below? When using React you are able to use MemoryRouter to pass initialEntries to mock a route change but I cannot find an alternative for preact-router. I looked at the Preact docs and the preact-router docs but I am unable to find a clear solution.
import 'preact/debug';
import { h, render } from 'preact';
import HomePage from './pages/homepage';
import Router from 'preact-router';
import AsyncRoute from 'preact-async-route';
import './styles/index.scss';
const App = () => (
<Router>
<HomePage path="/" />
<AsyncRoute
path="/admin"
getComponent={ () => import('./pages/admin').then(module => module.default) }
/>
</Router>
);
export default App;
This is a little old, but I figured I would share what I found.
The first and quickest thing to do is to just use the route function in preact-router.
import { render, route } from 'preact-router';
import App from './App';
describe('<App/>', () => {
it('renders admin', async () => {
const { container, findByText } = render(<App/>);
// Go to admin page
route('/admin');
// Wait for page to load since it's loaded async
await findByText(/Admin Page/);
// perform expectations.
});
});
While this works, I don't like that it relies on the brower's real history. Luckily, the <Router> component accepts a history prop of type CustomHistory. So you can use an in-memory implementation of a History API to make this happen. I think I've seen docs that suggest using the history package - however I had to make an adjustment
import { createMemoryHistory } from 'history';
class MemoryCustomHistory {
constructor(initialEntries = undefined) {
this.wrapped = createMemoryHistory({initialEntries});
}
get location() {
return this.wrapped.location;
}
// Listen APIs not quite compatible out of the box.
listen(callback) {
return this.wrapped.listen((locState) => callback(locState.location));
}
push(path) {
this.wrapped.push(path);
}
replace(path) {
this.wrapped.replace(path);
}
}
Next, update your app to accept a history property to pass to the <Router>
const App = ({history = undefined} = {}) => (
<Router history={history}>
<HomePage path="/" />
<AsyncRoute
path="/admin"
getComponent={ () => import('./pages/admin').then(module => module.default) }
/>
</Router>
);
Finally, just update the tests to wire your custom history to the app.
it('renders admin', async () => {
const history = new MemoryCustomHistory(['/admin]);
const { container, findByText } = render(<App history={history}/>);
// Wait for page to load since it's loaded async
await findByText(/Admin Page/);
// perform expectations.
});
Test Case
https://codesandbox.io/s/rr00y9w2wm
Steps to reproduce
Click on Topics
Click on Rendering with React
OR
Go to https://rr00y9w2wm.codesandbox.io/topics/rendering
Expected Behavior
match.params.topicId should be identical from both the parent Topics component should be the same as match.params.topicId when accessed within the Topic component
Actual Behavior
match.params.topicId when accessed within the Topic component is undefined
match.params.topicId when accessed within the Topics component is rendering
I understand from this closed issue that this is not necessarily a bug.
This requirement is super common among users who want to create a run in the mill web application where a component Topics at a parent level needs to access the match.params.paramId where paramId is a URL param that matches a nested (child) component Topic:
const Topic = ({ match }) => (
<div>
<h2>Topic ID param from Topic Components</h2>
<h3>{match.params.topicId}</h3>
</div>
);
const Topics = ({ match }) => (
<div>
<h2>Topics</h2>
<h3>{match.params.topicId || "undefined"}</h3>
<Route path={`${match.url}/:topicId`} component={Topic} />
...
</div>
);
In a generic sense, Topics could be a Drawer or Navigation Menu component and Topic could be any child component, like it is in the application I'm developing. The child component has it's own :topicId param which has it's own (let's say) <Route path="sections/:sectionId" component={Section} /> Route/Component.
Even more painful, the Navigation Menu needn't have a one-to-one relationship with the component tree. Sometimes the items at the root level of the menu (say Topics, Sections etc.) might correspond to a nested structure (Sections is only rendered under a Topic, /topics/:topicId/sections/:sectionId though it has its own normalized list that is available to the user under the title Sections in the Navigation Bar).
Therefore, when Sections is clicked, it should be highlighted, and not both Sections and Topics.
With the sectionId or sections path unavailable to the Navigation Bar component which is at the Root level of the application, it becomes necessary to write hacks like this for such a commonplace use case.
I am not an expert at all at React Router, so if anyone can venture a proper elegant solution to this use case, I would consider this to be a fruitful endeavor. And by elegant, I mean
Uses match and not history.location.pathname
Does not involve hacky approaches like manually parsing the window.location.xxx
Doesn't use this.props.location.pathname
Does not use third party libraries like path-to-regexp
Does not use query params
Other hacks/partial solutions/related questions:
React Router v4 - How to get current route?
React Router v4 global no match to nested route childs
TIA!
React-router doesn't give you the match params of any of the matched children Route , rather it gives you the params based on the current match. So if you have your Routes setup like
<Route path='/topic' component={Topics} />
and in Topics component you have a Route like
<Route path=`${match.url}/:topicId` component={Topic} />
Now if your url is /topic/topic1 which matched the inner Route but for the Topics component, the matched Route is still, /topic and hence has no params in it, which makes sense.
If you want to fetch params of the children Route matched in the topics component, you would need to make use of matchPath utility provided by React-router and test against the child route whose params you want to obtain
import { matchPath } from 'react-router'
render(){
const {users, flags, location } = this.props;
const match = matchPath(location.pathname, {
path: '/topic/:topicId',
exact: true,
strict: false
})
if(match) {
console.log(match.params.topicId);
}
return (
<div>
<Route exact path="/topic/:topicId" component={Topic} />
</div>
)
}
EDIT:
One method to get all the params at any level is to make use of context and update the params as and when they match in the context Provider.
You would need to create a wrapper around Route for it to work correctly, A typical example would look like
RouteWrapper.jsx
import React from "react";
import _ from "lodash";
import { matchPath } from "react-router-dom";
import { ParamContext } from "./ParamsContext";
import { withRouter, Route } from "react-router-dom";
class CustomRoute extends React.Component {
getMatchParams = props => {
const { location, path, exact, strict } = props || this.props;
const match = matchPath(location.pathname, {
path,
exact,
strict
});
if (match) {
console.log(match.params);
return match.params;
}
return {};
};
componentDidMount() {
const { updateParams } = this.props;
updateParams(this.getMatchParams());
}
componentDidUpdate(prevProps) {
const { updateParams, match } = this.props;
const currentParams = this.getMatchParams();
const prevParams = this.getMatchParams(prevProps);
if (!_.isEqual(currentParams, prevParams)) {
updateParams(match.params);
}
}
componentWillUnmount() {
const { updateParams } = this.props;
const matchParams = this.getMatchParams();
Object.keys(matchParams).forEach(k => (matchParams[k] = undefined));
updateParams(matchParams);
}
render() {
return <Route {...this.props} />;
}
}
const RouteWithRouter = withRouter(CustomRoute);
export default props => (
<ParamContext.Consumer>
{({ updateParams }) => {
return <RouteWithRouter updateParams={updateParams} {...props} />;
}}
</ParamContext.Consumer>
);
ParamsProvider.jsx
import React from "react";
import { ParamContext } from "./ParamsContext";
export default class ParamsProvider extends React.Component {
state = {
allParams: {}
};
updateParams = params => {
console.log({ params: JSON.stringify(params) });
this.setState(prevProps => ({
allParams: {
...prevProps.allParams,
...params
}
}));
};
render() {
return (
<ParamContext.Provider
value={{
allParams: this.state.allParams,
updateParams: this.updateParams
}}
>
{this.props.children}
</ParamContext.Provider>
);
}
}
Index.js
ReactDOM.render(
<BrowserRouter>
<ParamsProvider>
<App />
</ParamsProvider>
</BrowserRouter>,
document.getElementById("root")
);
Working DEMO
Try utilizing query parameters ? to allow the parent and child to access the current selected topic. Unfortunately, you will need to use the module qs because react-router-dom doesn't automatically parse queries (react-router v3 does).
Working example: https://codesandbox.io/s/my1ljx40r9
URL is structured like a concatenated string:
topic?topic=props-v-state
Then you would add to the query with &:
/topics/topic?topic=optimization&category=pure-components&subcategory=shouldComponentUpdate
✔ Uses match for Route URL handling
✔ Doesn't use this.props.location.pathname (uses this.props.location.search)
✔ Uses qs to parse location.search
✔ Does not involve hacky approaches
Topics.js
import React from "react";
import { Link, Route } from "react-router-dom";
import qs from "qs";
import Topic from "./Topic";
export default ({ match, location }) => {
const { topic } = qs.parse(location.search, {
ignoreQueryPrefix: true
});
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/topic?topic=rendering`}>
Rendering with React
</Link>
</li>
<li>
<Link to={`${match.url}/topic?topic=components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/topic?topic=props-v-state`}>
Props v. State
</Link>
</li>
</ul>
<h2>
Topic ID param from Topic<strong>s</strong> Components
</h2>
<h3>{topic && topic}</h3>
<Route
path={`${match.url}/:topicId`}
render={props => <Topic {...props} topic={topic} />}
/>
<Route
exact
path={match.url}
render={() => <h3>Please select a topic.</h3>}
/>
</div>
);
};
Another approach would be to create a HOC that stores params to state and children update the parent's state when its params have changed.
URL is structured like a folder tree: /topics/rendering/optimization/pure-components/shouldComponentUpdate
Working example: https://codesandbox.io/s/9joknpm9jy
✔ Uses match for Route URL handling
✔ Doesn't use this.props.location.pathname
✔ Uses lodash for object to object comparison
✔ Does not involve hacky approaches
Topics.js
import map from "lodash/map";
import React, { Fragment, Component } from "react";
import NestedRoutes from "./NestedRoutes";
import Links from "./Links";
import createPath from "./createPath";
export default class Topics extends Component {
state = {
params: "",
paths: []
};
componentDidMount = () => {
const urlPaths = [
this.props.match.url,
":topicId",
":subcategory",
":item",
":lifecycles"
];
this.setState({ paths: createPath(urlPaths) });
};
handleUrlChange = params => this.setState({ params });
showParams = params =>
!params
? null
: map(params, name => <Fragment key={name}>{name} </Fragment>);
render = () => (
<div>
<h2>Topics</h2>
<Links match={this.props.match} />
<h2>
Topic ID param from Topic<strong>s</strong> Components
</h2>
<h3>{this.state.params && this.showParams(this.state.params)}</h3>
<NestedRoutes
handleUrlChange={this.handleUrlChange}
match={this.props.match}
paths={this.state.paths}
showParams={this.showParams}
/>
</div>
);
}
NestedRoutes.js
import map from "lodash/map";
import React, { Fragment } from "react";
import { Route } from "react-router-dom";
import Topic from "./Topic";
export default ({ handleUrlChange, match, paths, showParams }) => (
<Fragment>
{map(paths, path => (
<Route
exact
key={path}
path={path}
render={props => (
<Topic
{...props}
handleUrlChange={handleUrlChange}
showParams={showParams}
/>
)}
/>
))}
<Route
exact
path={match.url}
render={() => <h3>Please select a topic.</h3>}
/>
</Fragment>
);
If you have a known set of child routes then you can use something like this:
Import {BrowserRouter as Router, Route, Switch } from 'react-router-dom'
<Router>
<Route path={`${baseUrl}/home/:expectedTag?/:expectedEvent?`} component={Parent} />
</Router>
const Parent = (props) => {
return (
<div >
<Switch>
<Route path={`${baseUrl}/home/summary`} component={ChildOne} />
<Route
path={`${baseUrl}/home/:activeTag/:activeEvent?/:activeIndex?`}
component={ChildTwo}
/>
</Switch>
<div>
)
}
In the above example Parent will get expectedTag, expectedEvent as the match params and there is no conflict with the child components and Child component will get activeTag, activeEvent, activeIndex as the parameters. Same name for params can also be used, I have tried that as well.
Try to do something like this:
<Switch>
<Route path="/auth/login/:token" render={props => <Login {...this.props} {...props}/>}/>
<Route path="/auth/login" component={Login}/>
First, the route with the parameter and after the link without parameter.
Inside my Login component I put this line of code console.log(props.match.params.token); to test and worked for me.
If you happen to use React.FC, there is a hook useRouteMatch.
For instance, parent component routes:
<div className="office-wrapper">
<div className="some-parent-stuff">
...
</div>
<div className="child-routes-wrapper">
<Switch>
<Route exact path={`/office`} component={List} />
<Route exact path={`/office/:id`} component={Alter} />
</Switch>
</div>
</div>
And in your child component:
...
import { useRouteMatch } from "react-router-dom"
...
export const Alter = (props) => {
const match = useRouteMatch()
const officeId = +match.params.id
//... rest function code
}
I'm using react-loadable v4.0.4 and webpack v3.5.1.
Here is my code,
import Dashboard from '../../scenes/dashboard/dashboard';
import ReactLoadable from 'react-loadable';
...
const yoPath = 'src/components/scenes/dashboard/dashboard';
const DashboardWrap = ReactLoadable({
loading: Dashboard,
loader: () => {
return new Promise((resolve, reject) =>
require.ensure(
[],
(require) => resolve(require(yoPath)),
(error) => reject(error),
'dashboardChunk'
)
)
}
});
And using react-router-dom v4.1.2, I've set Route as follows,
<Switch>
...
<Route exact path='/dashboard' component={DashboardWrap} />
...
</Switch>
I'm able to build the chunks for the respective component with the name dashboardChunk.
But while loading that component I'm getting the issues as follows.
In the console,
And the chunkfile,
Please let me know if I'm doing anything wrong.
I basically wanted to do code splitting, for that I've just done the following and it works fine.
I've created a common component(wrapper component) as follows,
import React, { Component } from 'react';
class Async extends Component {
componentWillMount = () => {
this.props.load.then((Component) => {
this.Component = Component
this.forceUpdate();
});
}
render = () => (
this.Component ? <this.Component.default /> : null
)
}
export default Async;
Then I've used above component as follows,
export const AniDemo = () => <Async load={import(/* webpackChunkName: "aniDemoChunk" */ "../../scenes/ani-demo/AniDemo.js")} />
export const Dashboard = () => <Async load={import(/* webpackChunkName: "dashboardChunk" */ "../../scenes/dashboard/Dashboard.js")} />
And using the above, I've made the changes in route as follows,
<Route exact path="/ani-demo" component={AniDemo} />
<Route exact path="/dashboard" component={Dashboard} />
With the help of the above changes that I made, I'm able to create chunks properly with the names that I've mentioned in the comments inside import statements ie aniDemoChunk.js and dashboardChunk.js respectively.
And these chunks load only when the respective component is called ie aniDemoChunk.js is loaded on browser only when AniDemo component is called or requested. Similarly for the Dashboard component respectively.
Note: If anyone is getting error re:Unexpected token import. So to support import() syntax just replace import to System.import() or else use babel-plugin-syntax-dynamic-import.
Webpack must be able to determine the imported path during static analysis. If you pass an argument into require, this is not possible.
It is best to put the actual path into require.ensure, i.e.
require.ensure(
['src/components/scenes/dashboard/dashboard']
require =>
resolve(require('src/components/scenes/dashboard/dashboard').default)
error => reject(error),
'dashboardChunk'
)
or use the newer dynamic import syntax. With the newer syntax you could simplify the above into:
Loadable({
loader: () => import(/* webpackChunkName: "dashboardChunk" */ 'src/components/scenes/dashboard/dashboard')
loading: MyLoader
})
Also, the loading argument should be a component to display while your asynchronous load is taking place, e.g. some kind of loading animation.