Conceptual article

Loading React Components Dynamically with Hooks

React

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:

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>;
}

A live example of this code is available on CodeSandbox.

Note: This example uses shortid to generate unique keys. As of this revision, shortid is now deprecated and nanoid is the recommended alternative.

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:

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:

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>
  );
}

A live example of this code is available on CodeSandbox.

Let’s break down the code above.

  • importView imports a view dynamically. It returns a NullView (Null object pattern) for an unmatched subreddit.
  • You then store components in views to render after you finished importing in useEffect.
  • loadViews inside useEffect imports views and stores them in the state with setViews.
  • Lastly, you need to wrap views with Suspense with a fallback to show when the components in views are 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:

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>
  );
}

A live example of this code is available on CodeSandbox.

The differences from the previous section are:

  • You are now dealing with an object, data, instead of the subreddit string.
  • 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:

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:

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>
  );
}

A live example of this code is available on CodeSandbox.

  • importView is the same as it was in previous examples except for the component location.
  • ChartList iterates an object where the name is a chart name and the value is the imported component.
  • The App state, charts, is an object to track components that are already loaded.
  • addChart imports a chart by name dynamically and add to the charts state, which is what you render.
  • loadPieChart and loadWaffleChart are convenience methods and you can memoize with useMemo.
  • return renders two buttons, and you need to wrap charts with Suspense.

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.

Creative Commons License