React lazy & Suspense API with Drupal

By drupov, 26 June, 2019

Since React 16.6 we have the possibility to load components on demand. This new feature is provided by the new method React.lazy(), which is part of the new Suspense API (WIP) of React.

Feeling lazy

Why would this be important?

Modern bundling tools like webpack are great, but there is a small caveat when using them, especially in big React applications - which most of them in the end are - the code packaged and loaded in the browser is synchronous. So on an initial page load your browser ends up loading a lot of code, which might never get executed at all. A very simple example with React is a component, which should be rendered only when a certain action on the frontend happens - up until now the code for that component is packaged and served to the browser initially. Components can be very different though - some of them are small, e.g. an <li>-item that receives its content via a prop - not a big deal. But imagine you have one that makes a call to an API and that query is quite long - on top of that a component can also have several methods in it that are needed to modify its’ output based on changing state. But that might never happen?

React.lazy() and Suspense API

Part of the new features shipped by the React.js team with the 16.6. Update allows us to optimize this loading process. We’ll create a very simple example with a React app and a Drupal backend to illustrate that.

Preparations for frontend and backend

Let’s make a folder for our frontend and backend apps:

mkdir react-lazy-suspense-with-drupal
cd react-lazy-suspense-with-drupal

Once inside the main folder, create the frontend (use create-react-app) and backend (use the composer template for Drupal projects) apps. I normally put them in separate “frontend” and “backend” folders in the top-level-folder (in this case the just created “react-lazy-suspense-with-drupal”).

Frontend preparations

Make sure to have at least React version 16.6 in the package.json file of the frontend app. As we’ll use React hooks in our example, it’s actually 16.7 (hooks came with 16.7), but the same demo can be done with normal class-based components:

  "dependencies": {
...
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
...
  },

We’ll be doing some data fetching from out Drupal backend, so let’s also get axios for that:

npm install axios

Backend preparations

Our backend will consist of Drupal core and let’s grab the graphql module to provide data from Drupal. Install the just created site as usual first. We’ll use the 3.x version of it for simplicity, as it’s shipping with full Drupal entity schema and we’ll show Drupal articles in our frontend in the example. Download and enable the module:

composer require drupal/graphql:3.x
drush en graphql_core

Note - you’ll need to setup the CORS settings for Drupal, so that the frontend can receive data for it on it’s own URL, see https://drupal.stackexchange.com/questions/245903/how-do-i-set-up-cors for more information on that.

We can now create some articles in Drupal. I am lazy (sometimes and it fits the current context of this article) and let devel_generate do that for me.

Creating our to-be-lazy-loaded-component in the React app

Let’s start with the component that will get loaded on demand. Create a file frontend/src/components/Articles.js. It can look something like this:

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function Articles() {
  const [data, setData] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'https://react-lazy-suspense-with-drupal.lndo.site/graphql?query={nodeQuery(limit:5){entities{entityId,entityLabel}}}',
      );

      setData(result.data.data.nodeQuery.entities);
    };

    fetchData();
  }, []);

  return (
      <ul>
        {data.map(item => (
            <li key={item.entityId}>{item.entityLabel}</li>
        ))}
      </ul>
  );
}

export default Articles;

First things first: you’ll probably use ApolloProvider to wrap your GraphQL query instead of passing it as a parameter, if done properly. But we want to focus on something else here.

OK, nothing too fancy with this component. We use useState to define out data constant and setData-callback (React hooks are really great!) and fetch data from Drupal with axios in the useEffect-hook (yes React hooks are indeed great!). In the end we return a list of the fetched data. At the end this is the component we want to load lazily.This is admittedly not a huge component, but in the real world a component can easily grow into such that loads/imports other components, each with its own logic queries etc.

Let’s jump to the main part of this demo now. We’ll put the code that will lazy-load our just created component in the App.js file of the React-app:

import React, { useState, Suspense } from 'react';
const Articles = React.lazy(() => import('./components/Articles'));

function App() {
  const [articlesVisible, setArticlesVisible] = useState(false);

  return (
    <React.Fragment>
      {articlesVisible ? (
        <Suspense fallback={<div>Loading...</div>}>
          <Articles />
        </Suspense>
      ) : (
        <button onClick={() => setArticlesVisible(true)}>Show articles</button>
      )}
    </React.Fragment>
  );
}

export default App;

Note that we load Suspense from the react-package at the top. Right after that we tell React that our Articles component will be lazy-loaded. For that to happen we return an import statement for the component in the lazy()-function. Up until now we would have imported it as we do with all our components in React normally, e.g. import Articles from './components/Articles'; and webpack would’ve bundled the components’ code together with all the rest.

With that we’re ready with our demo. To visualize the difference in the loading process check out the full filesize of the code without

Image
Without lazy suspense

and with the React.lazy() implementation:

Image
With lazy suspense

With lazy-loading you’ll notice that we have a smaller initial file-size - equal to the size of the bundle.js, produced by webpack - but now without the additional code included in our Article.js-component. This code get shipped to the browser only after the condition to show it is triggered by the user, exactly as expected.

This is pretty neat. If you want to play around with this, you can get a copy of the code over at this repository. Thanks for sticking until the end!

Comments