Photo by Holger Link on Unsplash
React v16.6.0 introduced React.lazy for code splitting.
Previous post, Loading React Components Dynamically on Demand showed how to load components dynamically enabling code splitting using import()
.
This is an updated post to show how to load components dynamically using React.lazy
, which wraps around import()
and retrieves a default component.
🗒 Note
I will skip problem statements in this entry to keep it short.
Problem Statements |
---|
Case 1 - Loading React Components Dynamically |
Case 2 – Handling Different Data Types |
Case 3 – Loading Components on Demand |
🚀 Case 1 – Loading React Components Dynamically
You can follow along in CodeSandbox& check the deployed site for coding splitting in devtools.
In the previous version, I’ve loaded components in componentDidMount
inside App.js
and stored components in a state called components
.
import React, { Component } from "react"; | |
import shortid from "shortid"; | |
import "./App.css"; | |
class App extends Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
components: [] | |
}; | |
} | |
addComponent = async type => { | |
console.log(`Loading ${type} component...`); | |
import(`./components/${type}.js`) | |
.then(component => | |
this.setState({ | |
components: this.state.components.concat(component.default) | |
}) | |
) | |
.catch(error => { | |
console.error(`"${type}" not yet supported`); | |
}); | |
}; | |
async componentDidMount() { | |
const { events } = this.props; | |
events.map(async type => await this.addComponent(type)); | |
} | |
render() { | |
const { components } = this.state; | |
if (components.length === 0) return <div>Loading...</div>; | |
const componentsElements = components.map(Component => ( | |
<Component key={shortid.generate()} /> | |
)); | |
return <div className="App">{componentsElements}</div>; | |
} | |
} | |
export default App; |
But there is no need to store the components in the state as you can simply use lazily loaded component in render with smaller code.
import React, { lazy, Suspense, Component } from "react"; | |
import ReactDOM from "react-dom"; | |
import * as Events from "./components/events"; | |
import shortid from "shortid"; | |
import "./styles.css"; | |
class App extends Component { | |
render() { | |
const { events } = this.props; | |
const components = events.map(event => { | |
const Component = Events[event] ? Events[event] : Events.NullEvent; | |
return <Component key={shortid.generate()} />; | |
}); | |
return ( | |
<div className="App"> | |
<Suspense fallback={<div>Loading...</div>}>{components}</Suspense> | |
</div> | |
); | |
} | |
} |
Line#3 imports all event components that are exported from index.js
, which basically does a named exports of lazily loaded components.
Line #12 checks if an event passed via prop exists. If an event doesn’t exist, it uses a NullEvent
(which returns an empty component) instead of checking for a non-existent event in “catch” as I did in previous implementation.
Line #18 uses Suspense to wrap dynamically loaded components and shows a fallback UI, <div>Loading...</div>
.
Suspense is used to wait for/show loading indicator in case it takes too long to load lazily loaded components.
⚠ Note : This version of Suspense is not for fetching data but only for code splitting.
Refer to Dan Abramov’s tweet.
And here is the updated *Event
components.
import { lazy } from "react"; | |
const PushEvent = lazy(() => import(`./PushEvent`)); | |
const ReleaseEvent = lazy(() => import(`./ReleaseEvent`)); | |
const StatusEvent = lazy(() => import(`./StatusEvent`)); | |
const NullEvent = lazy(() => import(`./NullEvent`)); | |
export { PushEvent, ReleaseEvent, StatusEvent, NullEvent }; |
import React from "react"; | |
const NullEvent = () => <></>; | |
export default NullEvent; |
import React from "react"; | |
const PushEvent = () => <div class="event">Push Event</div>; | |
export default PushEvent; |
import React from "react"; | |
const ReleaseEvent = () => <div class="event">Release Event</div>; | |
export default ReleaseEvent; |
import React from "react"; | |
const StatusEvent = () => <div class="event">Status Event</div>; | |
export default StatusEvent; |
index.js
lazily loads *Event
components and does a named exports so that events can be looked up as a dictionary.
Note that NullEvent
is a dumb component that doesn’t return anything using a React.Fragment shortcut <></>
.

🚀 Case 2 – Handling Different Data Types
You can follow along in CodeSandbox& check the deployed site for coding splitting in devtools.
This patterns now looks almost the same as the first case.
import React, { Component, Suspense } from "react"; | |
import shortid from "shortid"; | |
import * as Events from "./events"; | |
import "./App.css"; | |
class App extends Component { | |
render() { | |
const { events } = this.props; | |
const components = events.map(event => { | |
const Component = Events[event.type]; | |
const key = shortid.generate(); | |
return Component ? ( | |
<Component key={key} {...event} /> | |
) : ( | |
<Events.NullEvent key={key} /> | |
); | |
}); | |
return ( | |
<Suspense fallback={<div>Loading...</div>}> | |
<h2>Sung''s GitHub Events</h2> | |
<div>{components}</div> | |
</Suspense> | |
); | |
} | |
} | |
export default App; |
Instead of loading components in componentDidMount
in the previous version, the updated one takes advantage of React.lazy
and loads components in render
instead.
If a matching GitHub event component is found load it or else load a NullEvent
.
<Suspense />
wraps lazily loaded components as it did in case 1.
Here is are the event components for completeness.
import React from "react"; | |
const ForkEvent = ({ created_at: eventDate, repo, org, actor, payload }) => { | |
const { display_login: login, url: actorURL } = actor; | |
const { name: repoName, url: repoURL } = repo; | |
return ( | |
<div class="event"> | |
<h3> | |
Fork - (<small>{eventDate.toString()}</small>) | |
</h3> | |
<p> | |
<a href={actorURL}>{login}</a> has forked{" "} | |
<a href={repoURL}>{repoName}</a> | |
</p> | |
</div> | |
); | |
}; | |
export default ForkEvent; |
import { lazy } from "react"; | |
const ForkEvent = lazy(() => import(`./ForkEvent`)); | |
const NullEvent = lazy(() => import(`./NullEvent`)); | |
const PushEvent = lazy(() => import(`./PushEvent`)); | |
const WatchEvent = lazy(() => import(`./WatchEvent`)); | |
export { ForkEvent, NullEvent, PushEvent, WatchEvent }; |
import React from "react"; | |
// Null object pattern | |
const NullEvent = () => { | |
return <div />; | |
}; | |
export default NullEvent; |
import React from "react"; | |
import shortid from "shortid"; | |
const PushEvent = ({ created_at: eventDate, repo, org, actor, payload }) => { | |
const { display_login: login, url: actorURL } = actor; | |
const { commits } = payload; | |
const { name: repoName, url: repoURL } = repo; | |
return ( | |
<div class="event"> | |
<h3> | |
Push - (<small>{eventDate}</small>) | |
</h3> | |
<div> | |
<a href={actorURL}>{login}</a> has pushed to{" "} | |
<a href={repoURL}>{repoName}</a> | |
<Commits commits={commits} /> | |
</div> | |
</div> | |
); | |
}; | |
const Commits = ({ commits }) => { | |
return ( | |
<ol> | |
{commits.map(commit => ( | |
<li key={shortid.generate()}> | |
{commit.author.name} has commited with message "{commit.message}" | |
</li> | |
))} | |
</ol> | |
); | |
}; | |
export default PushEvent; |
import React from "react"; | |
const WatchEvent = ({ created_at: eventDate, repo, org, actor, payload }) => { | |
const { display_login: login, url: actorURL } = actor; | |
const { action } = payload; | |
const { name: repoName, url: repoURL } = repo; | |
return ( | |
<div class="event"> | |
<h3> | |
Watch - (<small>{eventDate.toString()}</small>) | |
</h3> | |
<p> | |
<a href={actorURL}>{login}</a> has {action} following{" "} | |
<a href={repoURL}>{repoName}</a> | |
</p> | |
</div> | |
); | |
}; | |
export default WatchEvent; |
*Event
components are the same as in the previous post and the difference is the index.js
, which exports lazily loaded components to enable event name matching by key in App.render()
.

🚀 Case 3 – Loading Components on Demand
You can follow along in CodeSandbox& check the deployed site for coding splitting in devtools.
A few changes made since the last post.
addView
Instead of loading a NullView
in a catch
statement, it’s now checked against a dictionary, which is better programming practice and makes the code more readable.
loadedComponents
is also changed from an array to an object for more efficient look up (from Array.includes to key lookup).
From this,
addView = async viewName => { | |
// Don't load more than once. | |
if (this.state.loadedComponents.includes(viewName)) return; | |
console.log(`Loading ${viewName} view...`); | |
import(`./views/${viewName}.js`) | |
.then(Component => { | |
this.setState({ | |
loadedComponents: this.state.loadedComponents.concat(viewName), | |
components: this.state.components.concat( | |
<Component.default | |
key={shortid.generate()} | |
data={this.props.data} | |
/> | |
) | |
}); | |
}) | |
.catch(error => { | |
this.setState({ | |
loadedComponents: this.state.loadedComponents.concat(viewName), | |
components: this.state.components.concat( | |
<NullView key={shortid.generate()} /> | |
) | |
}); | |
}); | |
}; |
To this.
addView = async viewName => { | |
const { loadedComponents } = this.state; | |
if (loadedComponents[viewName]) return; | |
const View = Views[viewName]; | |
const key = shortid.generate(); | |
const { data } = this.props; | |
const component = View ? ( | |
<View key={key} data={data} /> | |
) : ( | |
<Views.NullView key={key} /> | |
); | |
this.setState(prevState => ({ | |
components: [...prevState.components, component], | |
loadedComponents: { ...prevState.loadedComponents, [viewName]: true } | |
})); | |
}; |
render
render
is also changed to wrap dynamically loaded components with <Suspense />
.
render() { | |
const { components } = this.state; | |
return ( | |
<div className="App"> | |
... | |
<div className="views"> | |
<Suspense fallback={<div>Loading a view</div>}> | |
{components.length === 0 ? ( | |
<div>Nothing to display...</div> | |
) : ( | |
components | |
)} | |
</Suspense> | |
</div> | |
</div> | |
); | |
} |
All *View
components are the same so I will only show components/views/index.js
.
import { lazy } from "react"; | |
const GraphView = lazy(() => import(`./GraphView`)); | |
const NullView = lazy(() => import(`./NullView`)); | |
const TableView = lazy(() => import(`./TableView`)); | |
export { GraphView, NullView, TableView }; |
Just like previous two cases, index.js
exports lazily imported components as a named export so that view can be looked up by a key in addView
in App.js
.
👋 Parting Words
This is just an updated post as the previous version still works.
The differences are I’ve added index.js
to export lazily loaded components and use them to look up by a key to decide whether to load a matching component or a null component.
I tried not to make changes in dynamically loaded components not to confuse if you already read the previous post.
🛠 Resources
-
Case 1 – Loading React Components Dynamically
-
Case 2 – Handling Different Data Types
-
Case 3 – Loading Components on Demand
The post Loading React Components Dynamically on Demand using React.lazy appeared first on Sung's Technical Blog.
Top comments (0)