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.
Single build file and multiple file buildFrom 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 who read 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.
In this case, 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 loading 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 using 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.
Build with route-oriented and component-oriented approachThere 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 in 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
AnotherComponent
and use it in the
render
method of the component
MyComponent
.
import AnotherComponent from './another-component'; class MyComponent extends React.Component { render() { return <AnotherComponent/>; } }
Now import happens synchronously. We need a way to make it asynchronous.
This can be done using
dynamic imports .
MyComponent
code so that
AnotherComponent
asynchronously.
class MyComponent extends React.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
is 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 input, and on its basis returns a new component, with advanced behavior - note.) from my library you can abstract away from all these problems.
Loadable
works extremely simple.
The minimum that you need is to transfer to
Loadable
an object with
loader
properties — a function that performs dynamic component import and
LoadingComponent
— the component that will be shown during the loading process.
import Loadable from 'react-loadable'; function MyLoadingComponent() { return <div>Loading...</div>; } const LoadableAnotherComponent = Loadable({ loader: () => import('./another-component'), LoadingComponent: MyLoadingComponent }); class MyComponent extends React.Component { render() { return <LoadableAnotherComponent/>; } }
But what happens if the component load fails?
We will need to somehow handle the error and notify the user.
If an error occurs, it will be passed to LoadingComponent as a property of the component (prop).
function MyLoadingComponent({ 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.
Preventing "pre-boot blinking" component
Under certain conditions, component loading can occur very quickly (in less than 200 milliseconds); This will lead to a brief appearance on the screen of the preloader component, 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.
MyLoadingComponent
also has the property
pastDelay
which is set to
true
at the time when 200 milliseconds pass from the beginning of the component download
(I see that this solution is not fully working - after all, if the component is loaded for 400 milliseconds, then after 200 millisecond waiting, the preloader will seem on the screen for the same 200 milliseconds - the interpreter's thoughts) .
export default function MyLoadingComponent({ error, pastDelay }) { if (error) { return <div>Error!</div>; } else if (pastDelay) { return <div>Loading...</div>; } else { return null; } }
The value 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 moves the cursor over this button.
The component created by calling the
Loadable
function provides a static
preload
method for this purpose.
let LoadableMyComponent = Loadable({ loader: () => import('./another-component'), LoadingComponent: MyLoadingComponent, }); class MyComponent extends React.Component { state = { showComponent: false }; onClick = () => { this.setState({ showComponent: true }); }; onMouseOver = () => { LoadableMyComponent.preload(); }; render() { return ( <div> <button onClick={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 the
Loadable
function in the
Loadable
property,
serverSideRequirePath
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 the
serverSideRequirePath
property of the
serverSideRequirePath
object specifies the full path to the module defining this component. The function
Loadable
is available in the
flushServerSideRequires
, the call of which returns an array from the paths to all the modules of the loadable components of the current page. When you run a Webpack with the
--json
flag
--json
upon completion of the build, an
output-webpack-stats.json
file will be created
output-webpack-stats.json
stores detailed information about the build. Using this data we will be able to calculate which pieces of the assembly are necessary for the current page and connect them via
script
tags in the html file of the generated page (see the
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 completing 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, with
React Fiber
we 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, 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