Have you heard of Lazy Loading? Starting from React 16, you can easily implement it in all of your projects. Let's see how.
Understanding Lazy Loading. Why?
You may have heard of Lazy Loading before in reference to images. This technique ensures that images are only downloaded when they need to be shown to the user.
This concept can also be applied to components in React 16+ without requiring any extra modules or libraries. Lazily loading a component means that data regarding that piece of the app will not be downloaded until it's time to mount it onto the UI.
This is especially useful for larger React apps with multiple pages, routing, and bigger scopes, such as an admin portal that doesn't need to be downloaded if the user isn't an admin.
This will improve loading times and, consequently, user experience.
Using React.Suspense and React.lazy
The React component Suspense
ensures that a placeholder is displayed until ALL the children have finished loading. It can be thought of as the async/await
of components.
For more information, refer to the official documentation.
In summary, here is how to use it:
import { Suspense } from 'react'
import { Loader } from './components/Loader/Loader.tsx'
function App() {
return (
<>
<Suspense fallback={<Loader />}>
{/* your components */}
</Suspense>
</>
)
}
export default App
If you are also referencing the documentation, the Suspense
component takes two parameters: fallback
and children
.
In React, the children
prop refers to everything that goes inside the component tags, like so:
.
However, simply using Suspense
is not sufficient. While Suspense
can display a placeholder (in this case, the Loader
component) until everything is ready to be shown, it does not actually cause components to be lazily loaded.
To implement lazy loading on components, we must use dynamic imports.
If you are not familiar with dynamic imports, you may have been importing everything like this until now:
import Something, {SomethingElse} from "a module"
This technique is called static import, and it works well almost every time.
However, when we're lazy loading, we need something a bit more advanced. According to the MDN documentation on import()
, "dynamic imports are only evaluated when needed," which is precisely what we need. The import()
function returns a Promise
, and here's how you can use it to dynamically import an image.
const { default: logo } = await import("./assets/logo.png")
//alternative, no async/await needed:
const {default:logo} = import("./assets/logo.png").then((module)=> {
return {default: logo}
})
Dynamic imports explained
What the hell is this code? Let's take a closer look at it.
First, it's important to understand what is returned by the import()
function. Here are three examples:
console.log(await import("./assets/bg.png"))
/*
Module {
default: "/src/assets/bg.png"
Symbol(Symbol.toStringTag): "Module"
get default: ƒ ()
set default: ƒ ()
}
*/
console.log(await import("./assets/projects.json"))
/*
Module {
default: Array(4)
Symbol(Symbol.toStringTag): "Module"
get default: ƒ ()
set default: ƒ ()
}
*/
console.log(await import("./components/Card/Card.tsx"))
/*
Module {
Card: () => {}
//a function representing my component
Symbol(Symbol.toStringTag): "Module"
get default: ƒ ()
set default: ƒ ()
}
*/
The return value of import()
is a Module
that contains a getter, a setter, and Symbol
properties, which we don't need for this project, and another property. In the examples above, this property is either default
or Card
, which is the name of the component. The reason for this is that while the .json and .png files are counted as "export default
", the Card
component has a non-default export.
Therefore, if the module has a default export, we'll find it in the default
property. Otherwise, we will find a property with the name of our export. Here is the output of a file that has several exports.
//demo.ts
const aDefaultFunction = () => {
return "def"
}
export const MickeyMouse = () => {
return "not def"
}
export default aDefaultFunction
//dynamic import:
console.log(await import("./demo.ts"))
/* Module {
default: () => {return "def"}
MickeyMouse: () => {return "not def"}
Symbol(Symbol.toStringTag): "Module"
get default: ƒ ()
set default: ƒ ()
}
*/
So, letβs get back to our original code:
1 | const { default: logo } = await import("./assets/logo.png")
2 | //alternative, no async/await needed:
3 | import("./assets/logo.png").then((module)=> module.default).then(src => console.log(src))
To understand the first version of this code, we must start from the end of the first line. We are dynamically importing the logo.png
file, which returns a Promise
(refer to the previous snippets for the structure). The await
keyword unwraps the Promise
and leaves us only with a Module
.
At this point, we are destructuring the Module
, taking only the default
property and renaming it to logo
. This creates a const variable called logo
, which contains the source of the image (previously contained in the default
property).
In the second version, we are also importing dynamically, unwrapping the Promise
with .then
, and returning module.default
to a second then
method, which takes care of the console.log()
.
Now that we have a better understanding of what a dynamic import is, let's go back to React lazy loading.
Using dynamic imports with React.lazy()
We can pair dynamic imports with React.lazy()
. According to the official documentation, lazy
takes only one parameter called load
, which should be of type Promise
. This works perfectly with dynamic imports, whose return value is Promise
.
import {lazy} from "react"
const Component = lazy(()=> import("path/to/file.*"))
You might also see this written as:
import React from "react"
const Component = React.lazy(()=> import("path/to/file.*"))
This method works perfectly if your components are exported with export default
. However, many people (myself included) do not export their components with default
. If you are using TypeScript, this will result in an error:
Type 'Promise<typeof import("path/to/component")>' is not assignable to type 'Promise<{ default: ComponentType<any>; }>'.
Property 'default' is missing in type 'typeof import("path/to/component")' but required in type '{ default: ComponentType<any>; }'
If you are already proficient in TypeScript, you can solve this issue quickly. However, if you are not, here is a breakdown of the problem.
Earlier, we mentioned that the type of import()
is Promise
. We can roughly think of Module
as this interface:
interface Module {
default: any
[key: string]?: any // this allows the interface to have any key,
// so it can have the not default exports
get default()
set default()
}
This error is telling us that React.lazy
's load
parameter requires a Promise
, but the value we passed is missing the property default
(which exists in Module
). This is caused by not exporting in a default way. We can either switch to export default
or we can add a .then()
like this:
import React from "react"
const Component = React.lazy(() => import("path/to/Component.tsx")
.then(module => ({ default: module.Component })))
We did it! We lazily imported a component! How do we use it? Simply like we would normally do. Letβs put it all together:
import { Suspense, lazy } from 'react'
import { Loader } from './components/Loader/Loader.tsx'
const Component = lazy(() => import("path/to/Component.tsx")
.then(module => ({ default: module.Component })))
function App() {
return (
<>
<Suspense fallback={<Loader />}>
<Component/>
</Suspense>
</>
)
}
export default App
This concept can be further expanded using react-router-dom
and its own lazy loading capabilities, but that is beyond the scope of this article.
Lazy loading in React is an awesome feature that can significantly enhance the performance and user experience of your applications by downloading and rendering components only when they are needed. It's like a waiter who brings you your dish only when you're ready for it! And the best part? You can easily implement it in your projects without the need for extra modules or libraries. By combining the Suspense
component with dynamic imports and React.lazy()
, your app can load faster and smoother than ever before! So why not give it a try? Your users will surely appreciate it!