Table of Contents
In this article, you will explore the concepts of how to load components dynamically. You will also explore the concepts of how to load components on-demand.
URL: https://www.progressiverobot.com/react-loading-components-dynamically-hooks/
Introduction
Loading components dynamically is a technique that can replace writing import for many components. Rather than declaring every possible component that can be used, you can use a dynamic value for the path of a component.
You can additionally use _lazy-loading_ to serve the bundle of code that is necessary to the end-user at that particular moment. A smaller bundle size for the end-user should result in performance improvements.
React 16.6.0+ provides React.lazy and React.Suspsense to support lazy-loading React components. Instead of importing all the components, lazy-loading will allow you to only import additional components when they are needed.
In this article, you will explore the concepts of how to load components dynamically. You will also explore the concepts of how to load components on-demand.
Prerequisites
To complete this tutorial, you'll need:
- An understanding of JavaScript variables and functions. You can review the How To Code in JavaScript series to learn more.
- An understanding of importing, exporting, and rendering React components. You can review our How To Code in React.js series to learn more.
No local development is required.
CodeSandbox examples are provided for further experimentation.
Loading Components Dynamically
Reddit is a website with multiple _subreddits_ for different topics. Each subreddit follows the pattern of having an r/ prefix. Assume you are developing an application that displays views for three subreddits: r/reactjs, r/learnreactjs, and r/javascript.
Suppose you are showing a different component depending on a property, subredditsToShow:
[label src/App.js]
import React from 'react';
import shortid from 'shortid';
import LearnReactView from './views/learnreactView';
import ReactView from './views/reactView';
import JavaScriptView from './views/javascriptView';
import NullView from './views/NullView';
export default function App({ subredditsToShow }) {
const subredditElementList = subredditsToShow.map(
subreddit => {
switch (subreddit) {
case 'reactjs':
return <ReactView key={shortid.generate()} />;
case 'learnreactjs':
return (
<LearnReactView key={shortid.generate()} />
);
case 'javascript':
return (
<JavaScriptView key={shortid.generate()} />
);
default:
return (
<NullView key={shortid.generate()}>
{`"r/${subreddit}" - not implemented`}
</NullView>
);
}
}
);
return <div>{subredditElementList}</div>;
}
In this example, subredditsToShow is defined in index.js as:
const subredditsToShow = [
'reactjs',
'learnreactjs',
'pics',
'reactjs',
'learnreactjs',
'svelte',
'javascript',
'learnreactjs'
];
When running this application, you will observe:
[secondary_label Output]
r/reactjs
r/learnreactjs
"r/pics" - not implemented
r/reactjs
r/learnreactjs
"r/svelte" - not implemented
r/javascript
r/learnreactjs
pics and svelte are not implemented. There is no case to handle them, and there is no separate view component for that subreddit in your application. The NullView is displayed for these subreddits.
This switch/case approach works well for a handful of subreddits. However, handling additional subreddits will require you to:
- Add a new import – importing even unused ones.
- Update the switch component – produces unmaintainable code.
You can prevent those issues by loading components dynamically per subreddit and removing the switch statement, as shown below, using useEffect and useState:
[label src/App.js]
import React<^>, { lazy, useEffect, useState }<^> from 'react';
import shortid from 'shortid';
<^>const importView = subreddit =><^>
<^>lazy(() =><^>
<^>import(`./views/${subreddit}View`).catch(() =><^>
<^>import(`./views/NullView`)<^>
<^>)<^>
<^>);<^>
export default function App({ subredditsToShow }) {
<^>const [views, setViews] = useState([]);<^>
<^>useEffect(() => {<^>
<^>async function loadViews() {<^>
<^>const componentPromises =<^>
<^>subredditsToShow.map(async subreddit => {<^>
<^>const View = await importView(subreddit);<^>
<^>return <View key={shortid.generate()} />;<^>
<^>});<^>
<^>Promise.all(componentPromises).then(setViews);<^>
<^>}<^>
<^>loadViews();<^>
<^>}, [subredditsToShow]);<^>
return (
<^><React.Suspense fallback='Loading views...'><^>
<^><div className='container'>{views}</div><^>
<^></React.Suspense><^>
);
}
Let's break down the code above.
importViewimports a view dynamically. It returns aNullView(Null object pattern) for an unmatchedsubreddit.
- You then store components in
viewsto render after you finished importing inuseEffect.
loadViewsinsideuseEffectimports views and stores them in the state withsetViews.
- Lastly, you need to wrap views with Suspense with a fallback to show when the components in
viewsare loaded.
This concludes load components dynamically with strings. You will explore a more advanced example next.
Loading Components Dynamically with Objects
Let's consider a situation where you load a different "view" dynamically by matching against a data property.
The Reddit API exposes a JSON response for search results. Here is an example of a response when searching for "react hooks";
[seconary_label Output]
{
"data": {
"children": [
{
"data": {
"subreddit": "reactjs",
"title": "New tutorial for React hook",
"url": "..."
}
},
{
"data": {
"subreddit": "javascript",
"title": "React Hook Form",
"url": "..."
}
},
{
"data": {
"subreddit": "Frontend",
"title": "React hook examples",
"url": "..."
}
}
]
}
}
You can handle different subreddits by loading only views you have implemented:
[label src/App.js]
import React, { lazy, useEffect, useState } from 'react';
import shortid from 'shortid';
const importView = subreddit =>
lazy(() =>
import(`./views/${subreddit}View`).catch(() =>
import(`./views/NullView`)
)
);
<^>const searchSubreddit = async query =><^>
<^>fetch(<^>
<^>`https://www.reddit.com/search.json?q=${query}`<^>
<^>).then(_ => _.json());<^>
export default function App({ subredditsToShow }) {
const [views, setViews] = useState([]);
<^>const extractData = response =><^>
<^>response.data.children.map(({ data }) => data);<^>
useEffect(() => {
async function loadViews() {
<^>const subredditsToShow = await searchSubreddit(<^>
<^>'react hooks'<^>
<^>).then(extractData);<^>
<^>const componentPromises = subredditsToShow.map(<^>
<^>async data => {<^>
const View = await importView(data.subreddit);
return (
<^><View key={shortid.generate()} {...data} /><^>
);
<^>}<^>
<^>);<^>
Promise.all(componentPromises).then(setViews);
}
loadViews();
}, [subredditsToShow]);
return (
<React.Suspense fallback='Loading views...'>
<div className='container'>{views}</div>
</React.Suspense>
);
}
The differences from the previous section are:
- You are now dealing with an object,
data, instead of thesubredditstring.
- You are passing data down to each dynamic view with
<View key={shortid.generate()} {...data} />
Each view now gets a copy of the data as a prop.
Here is an example of reactjsView. It is a view designed specifically for the reactjs subreddit:
[label src/views/reactjsView.js]
import React from 'react';
import Layout from './Layout';
import styled, { css } from 'styled-components';
const Container = styled.article`
display: flex;
flex-direction: column;
`;
export default ({ subreddit, title, url }) => (
<Layout
css={css`
&:hover {
background-color: papayawhip;
}
`}
>
<Container>
<h3>{title}</h3>
<p>{`r/${subreddit}`}</p>
<a href={url}>-> Visit the site</a>
</Container>
</Layout>
);
Here is an example of reactjsView. It is a view designed specifically for the javascript subreddit:
[label src/views/javascriptView.js]
import React from 'react';
import Layout from './Layout';
import styled, { css } from 'styled-components';
const Container = styled.article`
display: flex;
flex-direction: row;
background-color: rgba(0, 0, 0, 0.1);
padding: 2rem;
& > * {
padding-left: 1rem;
}
`;
export default ({ subreddit, title, url }) => (
<Layout
css={css`
background-color: papayawhip;
`}
>
<Container>
<h4>{title}</h4>
<p>({`r/${subreddit}`})</p>
<a href={url}>-> Visit the site</a>
</Container>
</Layout>
);
Both of these views can use subreddit, title, and url provided by the data object.
This concludes loading components dynamically with objects.
Loading Components On-Demand
In the previous examples, you have loaded components automatically without a performance improvement.
You can improve this by sending JavaScript only when needed when a user performs an action.
Suppose that you need to show different types of charts for the following data:
const data = [
{
id: 'php',
label: 'php',
value: 372,
color: 'hsl(233, 70%, 50%)'
},
{
id: 'scala',
label: 'scala',
value: 363,
color: 'hsl(15, 70%, 50%)'
},
{
id: 'go',
label: 'go',
value: 597,
color: 'hsl(79, 70%, 50%)'
},
{
id: 'css',
label: 'css',
value: 524,
color: 'hsl(142, 70%, 50%)'
},
{
id: 'hack',
label: 'hack',
value: 514,
color: 'hsl(198, 70%, 50%)'
}
];
You can load the site faster without sending unused JavaScript and load charts only when needed:
import React, { lazy, useState } from 'react';
import shortid from 'shortid';
const importView = chartName =>
lazy(() =>
import(`./charts/${chartName}`)
.catch(() => import(`./charts/NullChart`))
);
const data = [ ... ];
const ChartList = ({ charts }) =>
Object.values(charts).map(Chart => (
<Chart key={shortid.generate()} data={data} />
));
export default function App() {
const [charts, setCharts] = useState({});
const addChart = chartName => {
if (charts[chartName]) return;
const Chart = importView(chartName);
setCharts(c => ({ ...c, [chartName]: Chart }));
};
const loadPieChart = () => addChart('Pie');
const loadWaffleChart = () => addChart('Waffle');
return (
<main>
<section className="container">
<button disabled={charts['Pie']}
onClick={loadPieChart}>
Pie Chart
</button>
<button disabled={charts['Waffle']}
onClick={loadWaffleChart}>
Waffle Chart
</button>
</section>
<section className="container">
<React.Suspense fallback="Loading charts...">
<div className="row">
<ChartList charts={charts} />
</div>
</React.Suspense>
</section>
</main>
);
}
importViewis the same as it was in previous examples except for the component location.
ChartListiterates an object where the name is a chart name and the value is the imported component.
- The
Appstate,charts, is an object to track components that are already loaded.
addChartimports a chart by name dynamically and add to thechartsstate, which is what you render.
loadPieChartandloadWaffleChartare convenience methods and you can memoize with useMemo.
returnrenders two buttons, and you need to wrap charts withSuspense.
src/charts/Pie.js and src/charts/Waffle.js are only loaded when the user clicks on the respective buttons.
This concludes loading components on-demand with React.lazy and React.Suspense.
Conclusion
In this article, you were introduced to loading components dynamically and loading components on-demand. These techniques can help improve maintenance and performance.
If you'd like to learn more about React, check out our React topic page for exercises and programming projects.