Implementing Lazy Loading with React 16+

REACT LAZY LOADINGREACTFRONTEND DEVELOPMENTREACT SUSPENSEREACT PERFORMANCE OPTIMIZATION

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: IN HERE .

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!