Loading React Components Dynamically with Hooks

Sung M. Kim

Dynamic sites need flexibility. Flexibility results in more code. More code means increased maintenance time. How can we keep our sites maintainable and fast?

One of the ways to load sites faster is to send browsers less JavaScript to process.

We can shave off a few bytes by lazy-loading React components (“components” hereafter). Such a dynamic loading can also make code maintainable (but not always).

We’ll take a look at how to load components dynamically, and check out some more advanced usage. Lastly, we’ll load only the components being used.

Table of Contents

Loading Components Dynamically

Suppose you are showing a different component depending on a property, subredditsToShow.

Try out here

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

We'll use shortid to generate unique keys because we don't know what can be a key for a component.

Whenever we want to handle a new subreddit, we need to

  • Add a new import - importing even unused ones.
  • Update the switch component - produces unmaintainable code.

We can prevent those issues by loading components dynamically per subreddit
and removing the switch statement as shown below using useEffect and useState.

Try out here

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.

  • importView imports a view dynamically. It returns a NullView (Null object pattern) for an unmatched subreddit.
  • We then store components in views to render after we finished importing in useEffect.
  • loadViews inside useEffect imports views and stores them in the state with setViews.
  • Lastly, we need to wrap views with Suspense) with a fallback to show when the components in views is.

We now know how to load components dynamically so let’s take a look at a more advanced scenario.

Handling Different Data Types

We can load a different “view” dynamically by matching against a data property.
Suppose that we’re dealing with the following JSON data.

{
  "data": {
    "children": [
      {
        "data": {
          "subreddit": "reactjs",
          "title": "Really good tutorial on using hooks",
          "url": "..."
        }
      },
      {
        "data": {
          "subreddit": "javascript",
          "title": "[Re-Post] React Hook Form",
          "url": "..."
        }
      },
      {
        "data": {
          "subreddit": "pics",
          "title": "Dogs vs. Cats",
          "url": "..."
        }
      }
    ]
  }
}

It's from an actual response from Reddit API

We can handle different subreddits by loading only views we’ve implemented.

Try out here

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:

  • We are now dealing with an object, data, instead of the subreddit string.
  • Passing data down to each dynamic view, <View key={shortid.generate()} {...data} />

Each view now gets a copy of the data as a prop and uses it anyway it wants to.

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

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

Loading components provided no benefit as we loaded them automatically.

Let’s see how we can send less JavaScript to the browser by loading the components only when a user takes an action.

Loading Components on Demand

So far, we’ve loaded components automatically without a performance improvement.

We can do better by sending JavaScript only when needed when a user performs an action.

Suppose that we 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%)'
  }
];

We can load the site fast without sending unused JavaScript and load charts only when needed.

Try out here

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>
  );
}
  • importView is the same as it was 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 we render.
  • loadPieChart and loadWaffleChart are convenience methods and you can memoize with useMemo.
  • return renders two buttons, and we need to wrap charts with Suspense.

Wrapping up

Now you know how to load React components dynamically on demand with the help of hooks.

A possible use case for dynamic loading is for a dashboard site where you load features only when a user accesses them.

Working Demos

  Tweet It

🕵 Search Results

🔎 Searching...