15

I've been trying to lazy load routes in React using React.lazy and Suspense. But some components are loading regardless of the current route, exactly: Feed, Profile and Settings.

Notice I don't actually want to lazy load Components like MenuAppBar and SnackAlert but if I import them normally and remove their Suspense, code-splitting straight doesn't even work and everything loads and the whole app is just a single chunk.

import {createMuiTheme, MuiThemeProvider} from "@material-ui/core";
import {yellow} from "@material-ui/core/colors";
import CssBaseline from "@material-ui/core/CssBaseline";
import axios from "axios";
import React, {lazy, Suspense, useEffect, useState} from "react";
import {BrowserRouter as Router, Route, Switch} from "react-router-dom";
import "./css/feed.css";
import "./css/style.css";

const Feed = lazy(() => import("./routes/Feed"));
const Profile = lazy(() => import("./routes/Profile"));
const Home = lazy(() => import("./routes/Home"));
const Settings = lazy(() => import("./routes/Settings"));
const NotFound = lazy(() => import("./routes/NotFound"));
const MenuAppBar = lazy(() => import("./components/MenuAppBar"));
const SnackAlert = lazy(() => import("./components/SnackAlert"));

const App: React.FC = () => {
    const [isLogged, setIsLogged] = useState(localStorage.getItem("token") ? true : false);

    const [user, setUser] = useState<User>(
        isLogged ? JSON.parse(localStorage.getItem("userInfo") as string) : {admin: false}
    );
    const [openError, setOpenError] = useState<boolean>(false);
    const [errorMsg, setErrorMsg] = useState<string>("");
    const [severity, setSeverity] = useState<Severity>(undefined);
    const [pwa, setPwa] = useState<any>(null);
    const [showBtn, setShowBtn] = useState<boolean>(false);
    const [isLight, setIsLight] = useState<boolean>(
        (JSON.parse(localStorage.getItem("theme") as string) as boolean) ? true : false
    );

    const theme: customTheme = {
        darkTheme: {
            palette: {
                type: "dark",
                primary: {
                    main: yellow[600]
                }
            }
        },
        lightTheme: {
            palette: {
                type: "light",
                primary: {
                    main: yellow[700]
                }
            }
        }
    };

    window.addEventListener("beforeinstallprompt", (event) => {
        event.preventDefault();
        setPwa(event);
        setShowBtn(true);
    });

    window.addEventListener("appinstalled", (e) => {
        setShowBtn(false);
        setErrorMsg("App installed!");
        setSeverity("success");
        setOpenError(true);
    });

    const handleClick = () => {
        if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
            setErrorMsg(`Please, open the share menu and select "Add to Home Screen"`);
            setSeverity("info");
            setOpenError(true);
        } else {
            if (pwa) {
                pwa.prompt();
                pwa.userChoice.then((choiceResult: {outcome: "accepted" | "refused"}) => {
                    if (choiceResult.outcome === "accepted") {
                        setErrorMsg("App downloading in the background..");
                        setSeverity("info");
                        setOpenError(true);
                    }
                    setPwa(null);
                });
            }
        }
    };

    useEffect(() => {
        const token: string | null = localStorage.getItem("token");
        let userInfo: User = JSON.parse(localStorage.getItem("userInfo") as string);
        if (userInfo && token && !userInfo.admin) {
            setUser(userInfo);
            setIsLogged(true);
        }
        if (isLogged) {
            axios
                .get("/api/auth/user", {
                    headers: {
                        "x-auth-token": `${token}`
                    }
                })
                .then((res) => {
                    if (!userInfo || !token) {
                        setUser(res.data as User);
                    }
                    localStorage.setItem(`userInfo`, JSON.stringify(res.data as User));
                    setIsLogged(true);
                })
                .catch((err) => {
                    if (err) {
                        setIsLogged(false);
                    }
                });
        } else {
            localStorage.removeItem("token");
            localStorage.removeItem("userInfo");
        }
    }, [isLogged]);

    return (
        <MuiThemeProvider theme={isLight ? createMuiTheme(theme.lightTheme) : createMuiTheme(theme.darkTheme)}>
            <CssBaseline />
            <Router>
                <Suspense fallback={<div></div>}>
                    <Route
                        path="/"
                        render={() => (
                            <>
                                <MenuAppBar
                                    isLogged={isLogged}
                                    setIsLogged={setIsLogged}
                                    user={user}
                                    setUser={setUser}
                                    isLight={isLight}
                                    setIsLight={setIsLight}
                                />
                                <SnackAlert severity={severity} errorMsg={errorMsg} setOpenError={setOpenError} openError={openError} />
                            </>
                        )}
                    />
                </Suspense>
                <Suspense fallback={<div></div>}>
                    <Switch>
                        <Route exact path="/" render={() => <Home />} />

                        <Route exact path="/profile/:id" render={() => <Profile />} />

                        <Route exact path="/feed" render={() => <Feed isLogged={isLogged} user={user} />} />

                        <Route
                            exact
                            path="/settings"
                            render={() => (
                                <Settings isLight={isLight} setIsLight={setIsLight} handleClick={handleClick} showBtn={showBtn} />
                            )}
                        />
                        <Route render={() => <NotFound />} />
                    </Switch>
                </Suspense>
            </Router>
        </MuiThemeProvider>
    );
};

export default App;
Akash Kumar Verma
  • 3,185
  • 2
  • 16
  • 32
Osama Adam
  • 381
  • 1
  • 3
  • 11

5 Answers5

14

You are wrapping your entire Switch in a single Suspense, so all components will be lazily loaded at the same time. You probably only want each to be fetched/loaded when the specific route is rendered the first time.

<Switch>
  <Route
    exact
    path="/"
    render={props => (
      <Suspense fallback={<div>Loading...<div>}>
        <Home {...props} />
      </Suspense>
    )}
  />
  <Route
    exact
    path="/profile/:id"
    render={props => (
      <Suspense fallback={<div>Loading...<div>}>
        <Profile {...props} />
      </Suspense>
    )}
  />
  <Route
    exact
    path="/feed"
    render={() => (
      <Suspense fallback={<div>Loading...<div>}>
        <Feed isLogged={isLogged} user={user} {...props} />
      </Suspense>
    )}
  />
  <Route
    exact
    path="/settings"
    render={() => (
      <Suspense fallback={<div>Loading...<div>}>
        <Settings
          isLight={isLight}
          setIsLight={setIsLight}
          handleClick={handleClick}
          showBtn={showBtn}
          {...props}
        />
      </Suspense>
    )}
  />
  <Route
    render={() => <NotFound />}
  />
</Switch>

There is a lot of repetition here, so it is practical to factor out the suspense into a HOC.

const withSuspense = (WrappedComponent, fallback) => props => (
  <Suspense fallback={fallback}>
    <WrappedComponent {...props} />
  </Suspense>
);

You can either decorate each perspective default export, i.e.

export default withSuspense(Home, <div>Loading...<div>);

App.js

...
<Switch>
  <Route exact path="/" render={props => <Home {...props} />} />

or decorate them in your App

const HomeWithSuspense = withSuspense(Home, <div>Loading...<div>);

...

<Switch>
  <Route
    exact
    path="/"
    render={props => <HomeWithSuspense {...props} />}
  />
  ...
</Switch>
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • 3
    Thanks for your reply really appreciate the time you put into help me with my problem. I've actually tried this approach before but I went ahead and reapplied Suspense to each component I want to lazy load but I still get the same random loading of the same 3 components I mentioned before. Could the components be loading because they have say a common child component that's also present in MenuAppBar so they're being loaded by proxy? I hope I'm making sense. Again really grateful for your reply. – Osama Adam Feb 17 '20 at 01:24
  • @drew is the initial statement that wrapping all the routes in a single Suspense will lazy load them all at the same time, correct? The React.Suspense documentation has examples that use this syntax: https://reactjs.org/docs/code-splitting.html#route-based-code-splitting – Dan Mar 13 '22 at 16:59
  • @Dan Yes. If you want to lazily load individual components then they each need their own suspense wrapper. It seems the React docs are now using RRDv6, but here's the official RRDv6 [Lazy Loading](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/lazy-loading?file=src/App.tsx) demo showing a per/route configuration. – Drew Reese Mar 13 '22 at 23:22
8

In case someone is having the same problem, the actual problem was that some of the components had other components within them which weren't exported as default and that's why they weren't being lazy-loaded.

So if you're having the same problem, you should check the import tree of the component you're trying to lazy-load and make sure every component in this tree is exported as default.

For more information refer to the named exports section in the react docs.

Thanks everyone for your help!

Osama Adam
  • 381
  • 1
  • 3
  • 11
1

That should work, I would look other problems, like build scripts, or some other piece of code using those same bundles. (e.g. the inheritance thing you mentioned in comments)

Jkarttunen
  • 6,764
  • 4
  • 27
  • 29
  • I managed to fix it. Sorry for not updating the post. The problem was that within the components that were not lazy loaded, some components weren't exported as default and React lazyload doesn't like that. I'll edit the main post right away in case someone is having the same problem as me. Thanks for the notification! – Osama Adam Jul 12 '20 at 14:06
1

Please try this once if above are worked

import React, { Suspense, lazy } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
const Home = lazy(() => import("./components/Home"));
const About = lazy(() => import("./components/About"));

const App = () => (
   <Router>
     <Suspense fallback={<div>Loading...</div>}>
       <Routes>
         <Route exact path='/' element={<Home/>}/>
         <Route exact path='/about' element={<About/>}/>
         </Routes>
     </Suspense>
   </Router>
 );

export default App
Gopala Raja Naika
  • 2,321
  • 23
  • 18
0

There's no need for the whole tree that you're trying to lazy load to have default imports and exports. The component tree with its unique dependencies will be bundled into lazy chunk by default.

For eg.

Component.js

import { x, y } from z
.....
export default Component

main.js

const Component = React.lazy(() => import('Component.js')

Here the main.js chunk will not include code any code from z or any of the code from Component.js and its unique dependencies

https://webpack.js.org/guides/code-splitting/#dynamic-imports https://create-react-app.dev/docs/code-splitting/#appjs