Introduction to React Loadable

Hi, Habr. I present to you a free translation of the article “Introducing React Loadable” by James Kyle . In it, James tells what a component-oriented approach to code separation is and represents the Loadable library he developed, a tool that allows you to implement this approach in React.js applications.

image
A single assembly file and assembly from several files

From the translator: I allowed myself not to translate some verbs and terms commonly used in untranslated transliterated form (such as “preloader” and “rendering”), I believe, they will be understood even by users reading only materials in Russian.

With the growth of the code base of your application, a moment comes when a single assembly file begins to negatively affect the speed of the initial page load. Splitting code into multiple files and dynamically loading them solves this problem.

Modern tools, such as Browserify and Webpack, cope well with this task.

At the same time, you have to decide on what principle the code fragments will be combined into one file and how your application will learn about the successful download of the next fragment and handle this event.

Route-oriented and component-oriented approach to code separation


Now almost universally used route-oriented approach. Those. Your code fragments are allocated in separate files based on the principle that the code in each file is necessary for the operation of a specific page (route) of your application. And the use of this approach is quite logical, because clicking on the link, and then waiting for the page to load is a standard and familiar action for the user.

But there is no limit to perfection, why not try to improve this approach?

For applications on React.js, routing is just a component. And this means that we can try to use a component-oriented approach to code separation.

image
Build with route-oriented and component-oriented approach

There is a mass of components whose code would be logical to allocate in a separate file. Modal windows, inactive tabs and many other interface elements are initially hidden and may not appear on the page at all if the user does not take certain actions.

So why do we have to load the code of these components (and, possibly, the code of third-party libraries to work with these components) immediately upon entering the page?

At the same time, with a component-oriented approach, the separation of code according to the routes of the application also works great, because, as we mentioned above, the route is just a component. So just do what's best for your application.

And of course, you would like to implement a component-oriented separation of the code did not have to spend a lot of effort. To select a component into a separate code fragment, it was enough to change a few lines in your application, and other “magic” would be executed “under the hood” without your participation.

Introduction to React Loadable


I wrote a small library - React Loadable, which allows you to do everything exactly as you want.

Suppose we have two components. We import the component AnotherComponentand use it in the rendercomponent method MyComponent.

import AnotherComponent from'./another-component';

classMyComponentextendsReact.Component{
  render() {
    return<AnotherComponent/>;
  }
}

Now import happens synchronously. We need a way to make it asynchronous.

This can be done using dynamic imports . Change the code MyComponentso that it AnotherComponentloads asynchronously.

classMyComponentextendsReact.Component{
  state = {
    AnotherComponent: null
  };

  componentWillMount() {
    import('./another-component').then(AnotherComponent => {
      this.setState({ AnotherComponent });
    });
  }
  
  render() {
    let {AnotherComponent} = this.state;
    if (!AnotherComponent) {
      return<div>Loading...</div>;
    } else {
      return<AnotherComponent/>;
    };
  }
}

However, such an implementation of asynchronous loading requires rather massive changes in the component code. But we have not yet described the case if the dynamic import fails. But what if we also need to render the application on the server side (server-side rendering)?

Using Loadable- a higher order component (if you work with react.js, you probably know this term and the principle of using such components, if not, read the relevant section of the documentation , or just note that this is a function that accepts the react component as an input , and on its basis returns a new component, with extended behavior - note.) from my library you can abstract away from all these problems.

Loadableworks extremely simple.

The minimum that is required of you is to transfer it to Loadablean object with properties loader— a function that dynamically imports a component and LoadingComponent— a component that will be shown during the loading process.

import Loadable from'react-loadable';

functionMyLoadingComponent() {
  return<div>Loading...</div>;
}

const LoadableAnotherComponent = Loadable({
  loader: () =>import('./another-component'),
  LoadingComponent: MyLoadingComponent
});

classMyComponentextendsReact.Component{
  render() {
    return<LoadableAnotherComponent/>;
  }
}

But what happens if the component load fails?

We will need to somehow handle the error and notify the user.
In case of an error, it will be passed to LoadingComponent as a property of the component (prop).

functionMyLoadingComponent({ error }) {
  if (error) {
    return<div>Error!</div>;
  } else {
    return<div>Loading...</div>;
  }
}

Webpack of the second version can work out of the box with dynamic imports - select the code that is connected via dynamic imports into separate files and load them in runtime. This means that you can easily experiment and select the code separation scheme that is optimal for the performance of your application.

You can see the working example by running the demo project from my repository . And if you want to understand in detail how Webpack 2 works with dynamic imports, check out the following sections of its official documentation.

Prevent “pre-boot flicker” component


Under certain conditions, component loading can occur very quickly (in less than 200 milliseconds); This will lead to a short-term appearance of the preloader component on the screen, which will instantly be replaced by the loaded component. The user's eye will not have time to see the preloader, it will only notice a certain “flicker” before loading the component.

A number of studies have shown that with this behavior, the user subjectively perceives the loading time longer than it actually happens. Those. for this case, it is better not to show the preloader at all, but just wait for the component to load.

MyLoadingComponentalso has a property pastDelaythat will be set to trueat the moment when 200 milliseconds pass from the beginning of the component loading(I see that this solution is not fully working - because if the component download takes 400 milliseconds, then after 200 millisecond waiting, the preloader will appear on the screen for the same 200 milliseconds - the interpreter's thoughts) .

exportdefaultfunctionMyLoadingComponent({ error, pastDelay }) {
  if (error) {
    return<div>Error!</div>;
  } elseif (pastDelay) {
    return<div>Loading...</div>;
  } else {
    returnnull;
  }
}

The value of 200 milliseconds is used by default, but you can change it by specifying the appropriate property:

Loadable({
  loader: () =>import('./another-component'),
  LoadingComponent: MyLoadingComponent,
  delay: 300
});

Preload


For further optimization, you can also preload the component so that by the time it is necessary to show the component on the page, its code has already been loaded. For example, you should show the component on the page after clicking on the button. In this case, you can start downloading the file with the code of this component as soon as the user places the cursor on this button.

The component created by a function call Loadableprovides a static method preloadfor this purpose.

let LoadableMyComponent = Loadable({
  loader: () =>import('./another-component'),
  LoadingComponent: MyLoadingComponent,
});

classMyComponentextendsReact.Component{
  state = { showComponent: false };

  onClick = () => {
    this.setState({ showComponent: true });
  };

  onMouseOver = () => {
    LoadableMyComponent.preload();
  };

  render() {
    return (
      <div><buttononClick={this.onClick}onMouseOver={this.onMouseOver}>
          Show loadable component
        </button>
        {this.state.showComponent && <LoadableMyComponent/>}
      </div>
    )
  }
}

Server side rendering


My library also supports server-side rendering.

To do this, in the object passed as an argument to a function Loadablein the property, serverSideRequirePathyou must specify the full path to the component.

import path from'path';

const LoadableAnotherComponent = Loadable({
  loader: () =>import('./another-component'),
  LoadingComponent: MyLoadingComponent,
  delay: 200,
  serverSideRequirePath: path.join(__dirname, './another-component')
});

In this case, if the page on which the component is present will be rendered on the server side, the component will be loaded synchronously, and if on the client side, it will be asynchronous.

In this case, in the server code we have to solve the problem of assembling a common script file that will be connected from the initially loaded html file.

As noted above, if we want the component to be rendered on the server side, then in the property serverSideRequirePathof the config object the full path to the module defining this component is indicated. A Loadablefunction is available in the library flushServerSideRequires, the call of which will return an array from the paths to all modules of the loadable components of the current page. When you run Webpack with the flag --jsonat the end of the assembly file will be createdoutput-webpack-stats.jsonstores detailed information about the assembly. Using this data, we will be able to calculate which pieces of the assembly are necessary for the current page and connect them via tags scriptin the html file of the generated page (see sample code ).

There remains the last, not yet solved task - to configure the Webpack so that it can resolve all this on the client. Sean, I will be waiting for your message after the publication of this article (here the author refers to Sean Larkin - the creator and main metainter of the Webpack project. Today, already finishing the translation, I came across this tweet and, as I understand it, this problem was solved, and the implementation of server-side rendering is even more simplified .

Potentially, I can imagine a lot of tools and technologies, the use of which in conjunction with my library can make your applications even more cool. For example, c, React Fiberwe can not only dynamically load the component code, but also implement “smart” rendering — i.e. determine the priority, which parts of the loaded component should be rendered first, and which parts should be postponed, i.e. only after the rendering of the higher priority elements has been completed.

In conclusion, I urge everyone to install and try my library, and also put Star to its repository on github .

yarn add react-loadable
# or
npm install --save react-loadable

Also popular now: