How to implement lazy loading and code splitting with react-router? - javascript

I am using React v17, and React Router v6+. When I load the page, the browser downloads all the js which is around 900kb, reducing the initial load time.
My routes are defined like so
const PrivateRoute = lazy(() => import("../utils/AuthenticatedRoutes"));
const Profile = lazy(() => import("../modules/Settings/User/Profile"));
const Buddies = lazy(() => import("../modules/Buddies/Buddies"));
const Buddy = lazy(() => import("../modules/Buddies/Buddy"));
const App = () => {
return (
<Suspense fallback={<Loader />}>
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/profile" element={<PrivateRoute render={<Profile />} />} />
<Route path="/buddies" element={<PrivateRoute render={<Buddy />} />} />
</Routes>
</Suspense>
)
}
This is the Private Route component
const PrivateRoute = ({ render }: { render: any }) => {
const location = useLocation();
const { loggedIn } = useSelector((state: RootState) => state.userReducer);
const pathname = location.pathname;
if (!loggedIn) {
return <Navigate to={`/login?redirectTo=${pathname}&search=${location.search}`} />;
}
return render;
};
Problem:
When I load any page on the app, even the one with no element in it, the entire JS of 999kb is downloaded and I don't think lazy loading should work that way.
How can I handle this ?

This is normal. You are wrapping the whole app with Suspense, which is directly resolved by your fallback while anything under it is suspended.
How to implement Suspense with react router ?
You should defined Suspense for each route you want to have lazy loading. So when the route is called, suspense will call the fallback while anything under it is suspended.
const PrivateRoute = lazy(() => import("../utils/AuthenticatedRoutes"));
const Profile = lazy(() => import("../modules/Settings/User/Profile"));
const Buddies = lazy(() => import("../modules/Buddies/Buddies"));
const Buddy = lazy(() => import("../modules/Buddies/Buddy"));
const App = () => {
return (
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/profile" element={<PrivateRoute render={<React.Suspense fallback={<Loader />}><Profile /></React.Suspense>} />} />
<Route path="/buddies" element={<PrivateRoute render={<React.Suspense fallback={<Loader />}><Buddy /></React.Suspense>} />} />
</Routes>
)
}
This is the same as the official documentation

I don't see any overt issues with the way you've implemented your code. The code splitting appears to be working.
Suggestion
I suggest refactoring the PrivateRoute component to be a layout route component instead of a wrapper component.
Example:
import { Navigate, Outlet, useLocation } from 'react-router-dom';
const PrivateRoute = () => {
const location = useLocation();
const { loggedIn } = useSelector((state: RootState) => state.userReducer);
const pathname = location.pathname;
if (!loggedIn) {
return <Navigate to={`/login?redirectTo=${pathname}&search=${location.search}`} />;
}
return <Outlet />;
};
...
const PrivateRoute = lazy(() => import("../utils/AuthenticatedRoutes"));
const Profile = lazy(() => import("../modules/Settings/User/Profile"));
const Buddies = lazy(() => import("../modules/Buddies/Buddies"));
const Buddy = lazy(() => import("../modules/Buddies/Buddy"));
const App = () => {
return (
<Suspense fallback={<Loader />}>
<Routes>
<Route path="/" element={<Landing />} />
<Route element={<PrivateRoute />}>
<Route path="/profile" element={<Profile />} />
<Route path="/buddies" element={<Buddy />} />
</Route>
</Routes>
</Suspense>
)
}
With this implementation I do see the Loader component "blip" on the screen momentarily for the first time each dynamically imported component is routed to.
I think it might be as Colin called out in a comment, that the dynamically imported components just aren't a significant portion of your overall app bundle size.

Related

React router v6 2elements with the same url path '/'

I am trying to have 2elements Dashboard and Login with the same url path ('/') but in two separated outlets when the user is logged in, i want to render the Dashboard and when is not render the Login page
import { lazy, Suspense, useMemo } from 'react'
import {
Route,
BrowserRouter,
Routes,
Navigate,
Outlet,
} from 'react-router-dom'
import * as PATHS from './constants/routes'
import UserContext from './context/user'
import useAuthListener from './hooks/use-auth-listener'
const Login = lazy(() => import('./pages/Login'))
const Register = lazy(() => import('./pages/Register'))
const NotFound = lazy(() => import('./pages/NotFound'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const PrivateOutlet = () => {
const { user } = useAuthListener()
return user ? <Outlet /> : <Navigate to={PATHS.LOGIN} />
}
const PublicOutlet = () => {
const { user } = useAuthListener()
return !user ? <Outlet /> : <Navigate to={PATHS.DASHBOARD} />
}
const App = () => {
const { user } = useAuthListener()
const value = useMemo(
() => ({
user,
}),
[{ user }]
)
return (
<UserContext.Provider value={value}>
<Suspense fallback={<p>Loading...</p>}>
<BrowserRouter>
<Routes>
<Route element={<PublicOutlet />}>
<Route path={PATHS.LOGIN} element={<Login />} />
<Route path={PATHS.SIGNUP} element={<Register />} />
</Route>
<Route element={<PrivateOutlet />}>
<Route path={PATHS.NOTFOUND} element={<NotFound />} />
<Route path={PATHS.DASHBOARD} element={<Dashboard />} />
</Route>
</Routes>
</BrowserRouter>
</Suspense>
</UserContext.Provider>
)
}
export default App
I already have a workaround of removing the PublicOutlet and adding a
<Route path={PATHS.LOGIN} element={!user ? <Login />} :<Dashboard />
but i would like to check if there is a better way of doing it in the PublicOutlet am using react router V6

React js cannot change page

I am learning react js and have a simple webpage with a few different views, and then a few different pages. I can change between the views just fine, but when I try to go to a page (in this case the login page, but the other have the same issue) the url path changes and the default landing page is viewed instead. The default page when loading the website is: http://localhost:3000/#/dashboard
If I press the login button, the code is:
<CButton
onClick={async () => {
window.location.href = "/login";
}}
>
<CIcon icon={cilUser} className="me-2" />
Login
</CButton>
Then it should redirect me to the /login page, but instead I get redirected to: http://localhost:3000/login#/dashboard
Why is this?
This is my App.js
import React, { Component, Suspense, useState, useMemo } from "react";
import { HashRouter, Route, Switch, withRouter } from "react-router-dom";
import "./scss/style.scss";
import { UserContext } from "./UserContext";
import { getProtectedAsset } from "./getProtectedAsset";
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse"></div>
</div>
);
// Containers
const DefaultLayout = React.lazy(() => import("./layout/DefaultLayout"));
// Pages
const Login = React.lazy(() => import("./views/pages/login/Login"));
const Page404 = React.lazy(() => import("./views/pages/page404/Page404"));
const Page500 = React.lazy(() => import("./views/pages/page500/Page500"));
function App() {
const [user, setUser] = useState({
userID: -1,
});
const value = useMemo(() => ({ user, setUser }), [user, setUser]);
return (
<UserContext.Provider value={value}>
<HashRouter>
<React.Suspense fallback={loading}>
<Switch>
<Route
exact
path="/login"
name="Login"
render={(props) => <Login {...props} />}
/>
<Route
exact
path="/register"
name="Register Page"
render={(props) => <Register {...props} />}
/>
<Route
exact
path="/register_club"
name="Register Club Page"
render={(props) => <Register_club {...props} />}
/>
<Route
exact
path="/404"
name="Page 404"
render={(props) => <Page404 {...props} />}
/>
<Route
exact
path="/500"
name="Page 500"
render={(props) => <Page500 {...props} />}
/>
<Route
path="/"
name="Home"
render={(props) => <DefaultLayout {...props} />}
/>
</Switch>
</React.Suspense>
</HashRouter>
</UserContext.Provider>
);
}
export default App;
Thank you for your help!
You are using the HashRouter so the URL you need to aim for is /#/login not /login, but that isn't the biggest problem here.
By using window.location.href you are bypassing the Router entirely and making the browser load the application again from scratch from a new URL.
Assuming you are using React Router version 6 (the current version), the equivalent of window.location.href is the navigate function from the useLocation hook.
However, since you are just creating something to click on that navigates, you should be simply using a <Link> component.
import { Link } from "react-router-dom";
<Link to="/login">
<CIcon icon={cilUser} className="me-2" />
Login
</Link>

Nested routing using protected routes is not working properly

earlier I posted a similar question: Method for checking admin is redirecting me to main page when trying to login
I tried to implement the protected route, inside a protectRoute.tsx:
import { Navigate, Outlet } from "react-router";
import { RootStateOrAny, useSelector } from "react-redux";
interface ProtectRouteProps {}
export default function ProtectRoute(props: ProtectRouteProps) {
const userSignin = useSelector((state: RootStateOrAny) => state.userSignin);
const { userInfo } = userSignin;
return userInfo?.user?.isAdmin ? <Outlet /> : <Navigate to='/' />;
}
I don't really know what ProtectRouteProps is and what I should put in it. Also in the routing part I did like he told me:
<Route path='/createItem' element={<ProtectRoute />} />
<Route path='/createItem' element={<CreateItem />} />
The problem now is that can't access CreateItem because is going on the ProtectRoute page that is an empty one. What should i do?
I don't really know what ProtectRouteProps is and what I should put in it.
There are no props. This is clear by the usage:
<Route path='/createItem' element={<ProtectRoute />} />
No props are passed to ProtectRoute. You can drop the props object:
import { Navigate, Outlet } from "react-router";
import { RootStateOrAny, useSelector } from "react-redux";
export default function ProtectRoute() {
const userSignin = useSelector((state: RootStateOrAny) => state.userSignin);
const { userInfo } = userSignin;
return userInfo?.user?.isAdmin ? <Outlet /> : <Navigate to='/' replace />;
}
The problem now is that can't access CreateItem because is going on
the ProtectRoute page that is an empty one. What should i do?
"Auth" routes are what are called layout routes. They apply some logic, perhaps some styled layout CSS, and render an Outlet for nested Route components to be rendered into. The nested Route components use the path prop for route matching.
Example:
<Route element={<ProtectRoute />}>
<Route path='/createItem' element={<CreateItem />} />
... other protected routes ...
</Route>
<Route exact path='/Login' element={<Login name="Login Page"></Login>}></Route>
<Route element={<Protected/>}>
<Route exact path='/' element={<Home/> }></Route>
<Route exact path='/Content' element={<Content />}></Route>
</Route>
<Route path='*' element={<Login/>} ></Route>
</Routes>
Create Protected.js
import { Navigate, Outlet } from 'react-router-dom';
const useAuth = ()=>{
if(localStorage.getItem("isLogged")){
const user = {loggedIN :true};
return user && user.loggedIN
}else{
const user = {loggedIN :false};
return user && user.loggedIN
}
}
const Protected = () => {
const isAuth = useAuth();
return isAuth ? <Outlet/>:<Navigate to={"/login"}/>
}
export default Protected

Trigger a rerender of parent component when a child component is rendered

I am using the following material-ui theme Paperbase and within the Header.js component, I have the following useEffect hook:
const [temperature, setTemperature] = useState([]);
const getTemperature= async () => {
try {
const response = await fetch('/get-temperature')
const tempData = await response.json();
setTemperature(tempData);
} catch (err) {
console.error(err.message);
}
};
useEffect(() => {
getTemperature();
}, []);
The main purpose of this, is to display the current temperature as info, within the header component, which gets displayed at first page load/render.
Now within my App.js below, I have the following return setup where the above Header component is called.
return (
<Router>
<UserProvider myinfo={myinfo}>
<Switch>
<Route path="/">
<ThemeProvider theme={theme}>
<div className={classes.root}>
<CssBaseline />
<nav className={classes.drawer}>
<Hidden xsDown implementation="css">
<Navigator />
</Hidden>
</nav>
<div className={classes.app}>
<Header
onDrawerToggle={handleDrawerToggle}
/>
<main className={classes.main}>
<Switch>
<Route exact path="/new-user"
render={(props) => <Content key={props.location.key} />}
/>
<Route exact path="/view-results"
render={(props) => <ViewResults key={props.location.key} />}
/>
</Switch>
</main>
</div>
</div>
</ThemeProvider>
</Route>
</Switch>
</UserProvider>
</Router>
);
My question is, how can I trigger a rerender of Header (parent) whenever the user routes to either /new-user or /view-results which in turn calls either Content.js or ViewResults.js, inorder to make the useEffect in Header.js refresh the data, from the REST api fetch and display the latest temperature in the header again?
Ideally anytime Content.js or ViewResults.js is rendered, ensure that Header.js getTemperature() is called.
Any help would be much appreciated.
Your current code is pretty close to a multi layout system. As being a component child of Route, you can access the current location via useLocation() or even the native window.location.pathname.
This is my example of multi layout React app. You can try to use it to adapt to your code.
The MainLayout use a fallback route when no path is specified. It also contains a Header and include a page
const Dispatcher = () => {
const history = useHistory();
history.push('/home');
return null;
};
const App = () => (
<BrowserRouter>
<Switch>
<Route
component={Dispatcher}
exact
path="/"
/>
<Route
exact
path="/login/:path?"
>
<LoginLayout>
<Switch>
<Route
component={LoginPage}
path="/login"
/>
</Switch>
</LoginLayout>
</Route>
<Route>
<MainLayout>
<Switch>
<Route
component={HomePage}
path="/home"
/>
</Switch>
</MainLayout>
</Route>
</Switch>
</BrowserRouter>
);
And here is the code for MainLayout
const MainLayout = ({ children }) => (
<Container
disableGutters
maxWidth={false}
>
<Header location={props.location} />
<Container
component="main"
maxWidth={false}
sx={styles.main}
>
{children}
</Container>
<Footer />
</Container>
);
Now that Header can be anything. You need to put a capture in this component
import { useLocation } from 'react-router-dom'
cont Header = (props) => {
const { pathname } = useLocation();
//alternatively you can access props.location
useEffect(() => {
if (pathname === '/new-user') {
getTemperature();
}
}, [pathname]);
};
Note that Header is not a direct descendant of Route therefore it cannot access the location directly via props. You need to transfer in chain
Route -> MainLayout -> Header
Or better use useLocation

dynamic basename with BrowserRouter in react-router-dom

Please I have an issue building a multi-tenant SaaS solution. For every tenant, I want them to use a subdomain, so i can get the subdomain from the url, make a call to a REST api that returns data about that tenant.
For example,
the admin (another app entirely - admin app) creates a tenant with domain name: tenant1.
In the tenant application on my local system, I was able to go to tenant1.localhost:3000. I get the url, and get the domain name. I then make a call with the domain to get the theme of tenant (this is stored in localStorage).
Unfortunately, we deploy on k8 in my company and so I couldn't mimic this behavior. So i have been advised by the devOps team to use subdomain in the context, thereby having localhost:3000/tenant1. Remember the tenant is dynamic, so i tried this:
<BrowserRouter basename={"/:tenant"}>
<Switch>
<Route exact path="/login" name="Login" component={Login} />
<Route exact path="/set-password/:token" name="Set Password" component={SetPassword} />
<PrivateRoute path="/" name="Default Layout" component={DefaultLayout} />
</Switch>
</BrowserRouter>
The solution above however makes my url to localhost:3000/:tenant/login
Please how can i use dynamic basename in the router, so it can accept:
localhost:3000/tenant1
localhost:3000/tenant3
localhost:3000/tenant2 etc.
It can allow any, my app handles wrong domain inputted
I finally used dynamic tenant with the following code
class App extends Component {
state = {
domain: ""
}
componentWillMount () {
const { domain } = this.state;
const parsedData = window.location.pathname.split("/");
let domain = parsedData[1];
this.setState({ domain: domain })
this.props.onGetTenant(domain);
}
render () {
const { domain } = this.state;
return () {
<BrowserRouter basename={"/"+domain}>
<Switch>
<Route exact path="/login" name="Login" component={Login} />
<Route exact path="/set-password/:token" name="Set Password" component={SetPassword} />
<PrivateRoute domain={domain} path="/" name="Default Layout" component={DefaultLayout} />
</Switch>
</BrowserRouter>
}
const mapStateToProps = state => {
const { tenant} = state;
return { tenant};
};
const mapDispatchToProps = (dispatch) => {
return {
onGetTenant: bindActionCreators( tenantActions.get, dispatch)
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App)
This worked for me using react >16 and react-router-dom v5
export const App = () => {
return (
<BrowserRouter>
<Switch>
<Route path="/:tenantId?" component={LayoutRoot} />
</Switch>
</BrowserRouter>
);
};
export const LayoutRoot = () => {
var { tenantId } = useParams();
//TODO: add some validation here and inform user if tenant is invalid
return (
<BrowserRouter basename={tenantId}>
<Switch>
<Route path="/login" component={LoginComponent} />
<Route path="/dashboard" component={DashboardComponent} />
</Switch>
</BrowserRouter>
);
};
You can render updates to your router's basename by using the key property. Any changes to the key value will cause the component to re-render.
Here's a code sandbox to demo:
https://codesandbox.io/s/react-router-dom-dynamic-basename-forked-hnkk0?file=/index.js
You can hover or inspect the links in the sandbox to verify that their href values are correctly updating after changing the basename. You can also see that the hrefs won't update if you remove the key property from Router.
import React, { useState } from "react";
import { render } from "react-dom";
import { BrowserRouter, Link } from "react-router-dom";
const Root = () => {
const [count, setCount] = useState(1);
const basename = `basename-${count}`;
return (
<BrowserRouter basename={basename} key={basename}>
<Link to="/link1">Link 1</Link>
<br />
<Link to="/link2">Link 2</Link>
<br />
Current basename: {basename}
<br />
<button onClick={() => setCount(count + 1)}>change basename</button>
</BrowserRouter>
);
};
render(<Root />, document.getElementById("root"));
Here's a codesandbox and the utility I wrote:
https://codesandbox.io/s/react-router-dom-dynamic-basename-xq9tj?file=/index.js
import urlJoin from 'url-join';
// it's important to have an identifier in their app
export const APP_ROOT_URL = '/my-app';
export const getBaseUrlPath = () => {
const currentPath = document.location.pathname || APP_ROOT_URL;
const startOfAppBase = currentPath.indexOf(APP_ROOT_URL);
let base = currentPath;
if (startOfAppBase !== -1) {
base = currentPath.substr(0, startOfAppBase);
}
base = urlJoin(base, APP_ROOT_URL);
return base;
};

Categories

Resources