@17media/react-loadable 中文文档教程

发布于 4年前 浏览 38 项目主页 更新于 3年前

React Loadable

用于使用动态导入加载组件的高阶组件。

Notice

这是我们需要的 react-loadable 的临时快速修复,一旦原始包有解决方案就应该被替换。

Install

yarn add react-loadable

Example

import Loadable from 'react-loadable';
import Loading from './my-loading-component';

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  loading: Loading,
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}

Happy Customers:

Users

如果你的公司或项目正在使用 React Loadable,请打开一个 PR 并添加 你自己到这个列表(请按字母顺序排列)

Also See:

  • react-loadable-visibility - Building on top of and keeping the same API as react-loadable, this library enables you to load content that is visible on the screen.



GUIDE

Guide

所以你已经有了你的 React 应用程序,你将它与 Webpack 捆绑在一起,事情 顺利。 但是有一天你注意到你的应用程序包变得如此之大 它正在减慢速度。

是时候开始对您的应用程序进行代码拆分了!

一个大包 vs 多个小包

代码拆分是一个大包的过程,其中包含你的整个 应用程序,并将它们分成多个较小的包,其中包含单独的 你的应用程序的一部分。

这似乎很难做到,但是像 Webpack 这样的工具已经内置了它,并且 React Loadable 旨在使其超级简单。

Route-based splitting vs. Component-based splitting

您将看到的一条常见建议是将您的应用程序分成单独的路线 并异步加载每一个。 对于许多应用程序来说,这似乎工作得很好—— 作为用户,点击链接并等待页面加载是一件很熟悉的事情 网络上的经验。

但我们可以做得更好。

使用 React 的大多数路由工具,路由只是一个组件。 有 他们没什么特别的(对不起瑞安和迈克尔 - 你就是 特别的)。 那么,如果我们优化围绕组件的拆分而不是 路线? 那会给我们带来什么?

路由与以组件为中心的代码拆分

事实证明:相当多。 除了路线之外,还有更多的地方 你可以很容易地拆分你的应用程序。 模式、选项卡和更多 UI 组件会隐藏内容,直到用户执行某些操作才能显示它。

示例:也许您的应用在选项卡组件中嵌入了地图。 为什么 你会不会每次都为父路由加载一个海量的映射库? 用户可能永远不会去那个标签?

更不用说所有可以推迟加载内容的地方了 优先内容加载完成。 页面底部的那个组件 它加载了一堆库:为什么要同时加载它 顶部的内容?

而且因为路由只是组件,我们仍然可以很容易地在 路由级别。

在您的应用程序中引入新的代码拆分点应该如此简单,以至于您 不要三思而后行。 这应该是改变几行的问题 代码和其他一切都应该自动化。

Introducing React Loadable

React Loadable 是一个小型库,可以进行以组件为中心的代码拆分 在 React 中非常容易。

Loadable 是高阶组件(创建组件的函数) 这使您可以在将任何模块渲染到您的应用程序之前动态加载它。

让我们想象两个组件,一个导入并渲染另一个。

import Bar from './components/Bar';

class Foo extends React.Component {
  render() {
    return <Bar/>;
  }
}

现在我们依赖于通过 import 同步导入的 Bar, 但是在我们去渲染它之前我们不需要它。 那我们为什么不推迟呢?

使用动态导入目前处于第 3 阶段的 tc39 提案) 我们可以修改我们的组件以异步加载 Bar

class MyComponent extends React.Component {
  state = {
    Bar: null
  };

  componentWillMount() {
    import('./components/Bar').then(Bar => {
      this.setState({ Bar: Bar.default });
    });
  }

  render() {
    let {Bar} = this.state;
    if (!Bar) {
      return <div>Loading...</div>;
    } else {
      return <Bar/>;
    };
  }
}

但那是一大堆工作,它甚至不能处理一堆案例。 当 import() 失败时怎么办? 服务器端渲染呢?

相反,您可以使用 Loadable 来抽象问题。

import Loadable from 'react-loadable';

const LoadableBar = Loadable({
  loader: () => import('./components/Bar'),
  loading() {
    return <div>Loading...</div>
  }
});

class MyComponent extends React.Component {
  render() {
    return <LoadableBar/>;
  }
}

Automatic code-splitting on import()

当你在 Webpack 2+ 中使用 import() 时,它会 自动代码拆分 你没有额外的配置。

这意味着您可以轻松地尝试新的代码拆分点 通过切换到 import() 并使用 React Loadable。 弄清楚执行的是什么 最适合您的应用程序。

Creating a great "Loading…" Component

呈现静态的“正在加载...”并不能与用户充分沟通。 你 还需要考虑错误状态、超时,并使其成为一个不错的选择 经验。

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

Loadable({
  loader: () => import('./WillFailToLoad'), // oh no!
  loading: Loading,
});

为了让这一切变得美好,您的加载组件收到一个 几个不同的道具。

Loading error states

当您的loader 失败时,您的加载组件 将收到一个 error 属性,它将是一个 Error 对象(否则它 将为 null)。

function Loading(props) {
  if (props.error) {
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else {
    return <div>Loading...</div>;
  }
}

Avoiding Flash Of Loading Component

有时组件加载非常快(<200 毫秒)并且仅加载屏幕 快速闪烁在屏幕上。

多项用户研究证明,这会导致用户感知事物 比他们实际花费的时间更长。 如果你不展示任何东西,用户就会察觉 它更快。

所以你的加载组件也会得到一个 pastDelay prop 只有当组件加载时间比一组时间长时才会为真 延迟

function Loading(props) {
  if (props.error) {
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

此延迟默认为 200ms,但您也可以自定义 Loadable 中延迟

Loadable({
  loader: () => import('./components/Bar'),
  loading: Loading,
  delay: 300, // 0.3 seconds
});

Timing out when the loader is taking too long

有时网络连接很糟糕,永远不会解决或失败,它们只是挂起 永远在那里。 这对用户来说很糟糕,因为他们不知道它是否应该 总是花这么长时间,或者他们是否应该尝试刷新。

加载组件将收到一个 timedOut proploader 已超时。

function Loading(props) {
  if (props.error) {
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.timedOut) {
    return <div>Taking a long time... <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

但是,默认情况下禁用此功能。 要打开它,你可以通过一个 timeout 选项Loadable

Loadable({
  loader: () => import('./components/Bar'),
  loading: Loading,
  timeout: 10000, // 10 seconds
});

Customizing rendering

默认情况下,Loadable 将呈现返回模块的 default 导出。 如果您想自定义此行为,您可以使用 render 选项

Loadable({
  loader: () => import('./my-component'),
  render(loaded, props) {
    let Component = loaded.namedExport;
    return <Component {...props}/>;
  }
});

Loading multiple resources

从技术上讲,你可以在 loader() 中做任何你想做的事,只要它 返回一个承诺并且你可以渲染一些东西。 但是写出来可能有点烦人。

为了更容易地并行加载多个资源,您可以使用 Loadable.Map

Loadable.Map({
  loader: {
    Bar: () => import('./Bar'),
    i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
  },
  render(loaded, props) {
    let Bar = loaded.Bar.default;
    let i18n = loaded.i18n;
    return <Bar {...props} i18n={i18n}/>;
  },
});

使用 Loadable.Map 时,需要 render() 方法。 它 将传递一个 loaded 参数,它将是一个匹配形状的对象 你的加载器

Preloading

作为优化,您还可以决定在组件获取之前预加载它 呈现。

例如,如果您需要在按下按钮时加载新组件, 当用户将鼠标悬停在按钮上时,您可以开始预加载组件。

Loadable 创建的组件公开了一个 静态 preload 方法 正是这样做的。

const LoadableBar = Loadable({
  loader: () => import('./Bar'),
  loading: Loading,
});

class MyComponent extends React.Component {
  state = { showBar: false };

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

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

  render() {
    return (
      <div>
        <button
          onClick={this.onClick}
          onMouseOver={this.onMouseOver}>
          Show Bar
        </button>
        {this.state.showBar && <LoadableBar/>}
      </div>
    )
  }
}



SERVER SIDE RENDERING

Server-Side Rendering

当你去渲染所有这些动态加载的组件时,你会得到什么 是一大堆加载屏幕。

这真的很糟糕,但好消息是 React Loadable 旨在 使服务器端渲染工作,就好像没有动态加载任何东西一样。

这是我们使用 Express 的起始服务器。

import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './components/App';

const app = express();

app.get('/', (req, res) => {
  res.send(`
    <!doctype html>
    <html lang="en">
      <head>...</head>
      <body>
        <div id="app">${ReactDOMServer.renderToString(<App/>)}</div>
        <script src="/dist/main.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000, () => {
  console.log('Running on http://localhost:3000/');
});

Preloading all your loadable components on the server

从服务器呈现正确内容的第一步是确保 当你去渲染时,你所有的可加载组件都已经加载了 他们。

为此,您可以使用 Loadable.preloadAll 方法。 它返回一个承诺,该承诺将在您所有可加载的时候解决 组件准备就绪。

Loadable.preloadAll().then(() => {
  app.listen(3000, () => {
    console.log('Running on http://localhost:3000/');
  });
});

Picking up a server-side rendered app on the client

这是事情变得有点棘手的地方。 所以让我们做好准备 一点。

为了让我们获取从服务器呈现的内容,我们需要 所有用于在服务器上呈现的相同代码。

为此,我们首先需要可加载组件告诉我们它们是哪些模块 正在渲染。

Declaring which modules are being loaded

LoadableLoadable.Map 用于告诉我们哪些模块是我们的 组件正在尝试加载:opts.modulesopts.webpack

Loadable({
  loader: () => import('./Bar'),
  modules: ['./Bar'],
  webpack: () => [require.resolveWeak('./Bar')],
});

但是不要太担心这些选项。 React Loadable 包括一个 Babel 插件 为您添加它们。

只需将 react-loadable/babel 插件添加到您的 Babel 配置中:

{
  "plugins": [
    "react-loadable/babel"
  ]
}

现在这些选项将自动提供。

Finding out which dynamic modules were rendered

接下来我们需要找出在请求时实际渲染了哪些模块 进来。

为此,有 Loadable.Capture 组件可以 用于收集所有呈现的模块。

import Loadable from 'react-loadable';

app.get('/', (req, res) => {
  let modules = [];

  let html = ReactDOMServer.renderToString(
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
      <App/>
    </Loadable.Capture>
  );

  console.log(modules);

  res.send(`...${html}...`);
});

Mapping loaded modules to bundles

为了确保客户端加载所有呈现的模块 服务器端,我们需要将它们映射到 Webpack 创建的包。

这分为两部分。

首先,我们需要 Webpack 来告诉我们每个模块位于哪个 bundle 中。 为了 这就是 React Loadable Webpack 插件

react-loadable/webpack 导入 ReactLoadablePlugin 并包含它 在你的 webpack 配置中。 向它传递一个 filename 用于存储 JSON 数据的位置 关于我们的捆绑包。

// webpack.config.js
import { ReactLoadablePlugin } from 'react-loadable/webpack';

export default {
  plugins: [
    new ReactLoadablePlugin({
      filename: './dist/react-loadable.json',
    }),
  ],
};

然后我们将返回到我们的服务器并使用这些数据将我们的模块转换为 捆绑。

要从模块转换为捆绑包,请导入 getBundles 来自 react-loadable/webpack 的方法和来自 Webpack 的数据。

import Loadable from 'react-loadable';
import { getBundles } from 'react-loadable/webpack'
import stats from './dist/react-loadable.json';

app.get('/', (req, res) => {
  let modules = [];

  let html = ReactDOMServer.renderToString(
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
      <App/>
    </Loadable.Capture>
  );

  let bundles = getBundles(stats, modules);

  // ...
});

然后我们可以将这些包呈现到 HTML 中的

在主包之前包含包很重要,这样 它们可以在应用程序呈现之前由浏览器加载。

然而,由于 Webpack 清单(包括解析包的逻辑)位于 主包,它需要被提取到它自己的块中。

这很容易用 CommonsChunkPlugin

// webpack.config.js
export default {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    })
  ]
}

注意:从 Webpack 4 开始,CommonsChunkPlugin 已经删除并且清单不再需要提取。

let bundles = getBundles(stats, modules);

res.send(`
  <!doctype html>
  <html lang="en">
    <head>...</head>
    <body>
      <div id="app">${html}</div>
      <script src="/dist/manifest.js"></script>
      ${bundles.map(bundle => {
        return `<script src="/dist/${bundle.file}"></script>`
        // alternatively if you are using publicPath option in webpack config
        // you can use the publicPath value from bundle, e.g:
        // return `<script src="${bundle.publicPath}"></script>`
      }).join('\n')}
      <script src="/dist/main.js"></script>
    </body>
  </html>
`);

Preloading ready loadable components on the client

我们可以在 客户端预加载页面上包含的可加载组件。

Loadable.preloadAll(),它返回一个承诺, 这在分辨率上意味着我们可以滋润我们的应用程序。

// src/entry.js
import React from 'react';
import ReactDOM from 'react-dom';
import Loadable from 'react-loadable';
import App from './components/App';

Loadable.preloadReady().then(() => {
  ReactDOM.hydrate(<App/>, document.getElementById('app'));
});

Now server-side rendering should work perfectly!



API DOCS

API Docs

Loadable

用于在之前动态加载模块的高阶组件 rendering it,一个loading组件被渲染 当模块不可用时。

const LoadableComponent = Loadable({
  loader: () => import('./Bar'),
  loading: Loading,
  delay: 200,
  timeout: 10000,
});

这将返回一个 LoadableComponent

Loadable.Map

允许您并行加载多个资源的高阶组件。

Loadable.Map 的 opts.loader 接受一个函数对象,并且 需要一个 opts.render 方法。

Loadable.Map({
  loader: {
    Bar: () => import('./Bar'),
    i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
  },
  render(loaded, props) {
    let Bar = loaded.Bar.default;
    let i18n = loaded.i18n;
    return <Bar {...props} i18n={i18n}/>;
  }
});

使用 Loadable.Map 时,render() 方法的 loaded 参数将是一个 与您的 loader 具有相同形状的对象。

Loadable and Loadable.Map Options

opts.loader

返回加载模块的承诺的函数。

Loadable({
  loader: () => import('./Bar'),
});

当与 Loadable.Map 一起使用时,它接受这些对象 类型的功能。

Loadable.Map({
  loader: {
    Bar: () => import('./Bar'),
    i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
  },
});

Loadable.Map 一起使用时,您还需要传递一个 opts.render 函数。

opts.loading

在模块加载时呈现的 LoadingComponent 加载或出错时。

Loadable({
  loading: LoadingComponent,
});

这个选项是必需的,如果你不想渲染任何东西,返回null

Loadable({
  loading: () => null,
});

opts.delay

通过之前等待的时间(以毫秒为单位) props.pastDelay 到您的loading 成分。 这默认为 200

Loadable({
  delay: 200
});

阅读有关延迟的更多信息

opts.timeout

通过之前等待的时间(以毫秒为单位) props.timedOut 到您的loading 组件。 这是默认关闭的。

Loadable({
  timeout: 10000
});

阅读有关超时的更多信息

opts.render

自定义加载模块渲染的函数。

接收 loaded,这是 opts.loader 的解析值 和 props 是传递给 LoadableComponent

Loadable({
  render(loaded, props) {
    let Component = loaded.default;
    return <Component {...props}/>;
  }
});

opts.webpack

一个可选函数,它返回一个 Webpack 模块 ID 数组,你可以 使用 require.resolveWeak 获取。

Loadable({
  loader: () => import('./Foo'),
  webpack: () => [require.resolveWeak('./Foo')],
});

这个选项可以通过 Babel 插件 实现自动化。

opts.modules

一个可选数组,其中包含用于导入的模块路径。

Loadable({
  loader: () => import('./my-component'),
  modules: ['./my-component'],
});

这个选项可以通过 Babel 插件 实现自动化。

LoadableComponent

这是 LoadableLoadable.Map 返回的组件。

const LoadableComponent = Loadable({
  // ...
});

传递给该组件的道具将直接传递给 通过 opts.render 动态加载组件。

LoadableComponent.preload()

这是 LoadableComponent 上的一个静态方法,它可以 用于提前加载组件。

const LoadableComponent = Loadable({...});

LoadableComponent.preload();

这会返回一个承诺,但你应该避免等待那个承诺 决心更新您的用户界面。 在大多数情况下,它会造成糟糕的用户体验。

阅读有关预加载的更多信息

LoadingComponent

这是您传递给 opts.loading 的组件。

function LoadingComponent(props) {
  if (props.error) {
    // When the loader has errored
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.timedOut) {
    // When the loader has taken longer than the timeout
    return <div>Taking a long time... <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.pastDelay) {
    // When the loader has taken longer than the delay
    return <div>Loading...</div>;
  } else {
    // When the loader has just started
    return null;
  }
}

Loadable({
  loading: LoadingComponent,
});

阅读有关加载组件的更多信息

props.error

传递给 Error 对象LoadingComponentloader 失败。 当没有错误时,null 是 通过。

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

阅读有关错误的更多信息

props.retry

loader 失败,用于重试加载组件。

function LoadingComponent(props) {
  if (props.error) {
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else {
    return <div>Loading...</div>;
  }
}

阅读有关错误的更多信息

props.timedOut

设置后传递给 LoadingComponent 的布尔属性 超时

function LoadingComponent(props) {
  if (props.timedOut) {
    return <div>Taking a long time...</div>;
  } else {
    return <div>Loading...</div>;
  }
}

阅读有关超时的更多信息

props.pastDelay

设置后传递给 LoadingComponent 的布尔属性 延迟

function LoadingComponent(props) {
  if (props.pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

阅读有关延迟的更多信息

Loadable.preloadAll()

这将调用所有 LoadableComponent.preload 递归方法 直到全部解决。 允许您预加载所有动态 服务器等环境中的模块。

Loadable.preloadAll().then(() => {
  app.listen(3000, () => {
    console.log('Running on http://localhost:3000/');
  });
});

重要的是要注意,这需要您声明所有可加载的 模块初始化时的组件而不是你的应用程序正在运行时 呈现。

好:

// During module initialization...
const LoadableComponent = Loadable({...});

class MyComponent extends React.Component {
  componentDidMount() {
    // ...
  }
}

差:

// ...

class MyComponent extends React.Component {
  componentDidMount() {
    // During app render...
    const LoadableComponent = Loadable({...});
  }
}

注意: Loadable.preloadAll() 如果你有多个则将不起作用 react-loadable 在您的应用程序中的副本。

阅读有关在服务器上预加载的更多信息

Loadable.preloadReady()

检查浏览器中已经加载的模块并调用匹配的 LoadableComponent.preload 方法。

Loadable.preloadReady().then(() => {
  ReactDOM.hydrate(<App/>, document.getElementById('app'));
});

阅读有关客户端预加载的更多信息

Loadable.Capture

用于报告渲染了哪些模块的组件。

接受为每个 moduleName 调用的 report 道具 通过 React Loadable 呈现。

let modules = [];

let html = ReactDOMServer.renderToString(
  <Loadable.Capture report={moduleName => modules.push(moduleName)}>
    <App/>
  </Loadable.Capture>
);

console.log(modules);

阅读更多关于捕获渲染模块的信息

Babel Plugin

为 每个可加载的组件都是需要记住的大量手动工作。

相反,您可以将 Babel 插件添加到您的配置中,它会自动执行 你:

{
  "plugins": ["react-loadable/babel"]
}

输入

import Loadable from 'react-loadable';

const LoadableMyComponent = Loadable({
  loader: () => import('./MyComponent'),
});

const LoadableComponents = Loadable.Map({
  loader: {
    One: () => import('./One'),
    Two: () => import('./Two'),
  },
});

输出

import Loadable from 'react-loadable';
import path from 'path';

const LoadableMyComponent = Loadable({
  loader: () => import('./MyComponent'),
  webpack: () => [require.resolveWeak('./MyComponent')],
  modules: [path.join(__dirname, './MyComponent')],
});

const LoadableComponents = Loadable.Map({
  loader: {
    One: () => import('./One'),
    Two: () => import('./Two'),
  },
  webpack: () => [require.resolveWeak('./One'), require.resolveWeak('./Two')],
  modules: [path.join(__dirname, './One'), path.join(__dirname, './Two')],
});

阅读更多关于声明模块的信息

Webpack Plugin

为了发送正确的包 在服务器端渲染时,你需要 React Loadable Webpack 插件  为您提供模块到捆绑包的映射。

// webpack.config.js
import { ReactLoadablePlugin } from 'react-loadable/webpack';

export default {
  plugins: [
    new ReactLoadablePlugin({
      filename: './dist/react-loadable.json',
    }),
  ],
};

这将创建一个文件 (opts.filename),您可以将其导入地图模块 捆绑。

阅读更多关于将模块映射到包的信息

getBundles

react-loadable/webpack 导出的方法,用于将模块转换为 捆绑。

import { getBundles } from 'react-loadable/webpack';

let bundles = getBundles(stats, modules);

阅读更多关于将模块映射到包的信息



FAQ

FAQ

How do I avoid repetition?

每次使用时指定相同的loading组件或delay Loadable() 快速重复。 相反,你可以用你的包装 Loadable 自己的高阶组件(HOC)来设置默认选项。

import Loadable from 'react-loadable';
import Loading from './my-loading-component';

export default function MyLoadable(opts) {
  return Loadable(Object.assign({
    loading: Loading,
    delay: 200,
    timeout: 10000,
  }, opts));
};

然后你去使用的时候指定一个loader就可以了。

import MyLoadable from './MyLoadable';

const LoadableMyComponent = MyLoadable({
  loader: () => import('./MyComponent'),
});

export default class App extends React.Component {
  render() {
    return <LoadableMyComponent/>;
  }
}

不幸的是,目前使用包装的 Loadable 中断 react-loadable/babel 所以在这种情况下你必须添加所需的属性(moduleswebpack) 手动。

import MyLoadable from './MyLoadable';

const LoadableMyComponent = MyLoadable({
  loader: () => import('./MyComponent'),
  modules: ['./MyComponent'],
  webpack: () => [require.resolveWeak('./MyComponent')],
});

export default class App extends React.Component {
  render() {
    return <LoadableMyComponent/>;
  }
}

How do I handle other styles .css or sourcemaps .map with server-side rendering?

当您调用 getBundles 时,它可能返回的文件类型不是 JavaScript 取决于您的 Webpack 配置。

要处理此问题,您应该手动过滤到文件扩展名 你关心:

let bundles = getBundles(stats, modules);

let styles = bundles.filter(bundle => bundle.file.endsWith('.css'));
let scripts = bundles.filter(bundle => bundle.file.endsWith('.js'));

res.send(`
  <!doctype html>
  <html lang="en">
    <head>
      ...
      ${styles.map(style => {
        return `<link href="/dist/${style.file}" rel="stylesheet"/>`
      }).join('\n')}
    </head>
    <body>
      <div id="app">${html}</div>
      <script src="/dist/main.js"></script>
      ${scripts.map(script => {
        return `<script src="/dist/${script.file}"></script>`
      }).join('\n')}
    </body>
  </html>
`);

React Loadable

A higher order component for loading components with dynamic imports.

Notice

This is a temporary quick-fix of react-loadable for our need, and should be replaced once the original package has solution.

Install

yarn add react-loadable

Example

import Loadable from 'react-loadable';
import Loading from './my-loading-component';

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  loading: Loading,
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}

Happy Customers:

Users

If your company or project is using React Loadable, please open a PR and add yourself to this list (in alphabetical order please)

Also See:

  • react-loadable-visibility - Building on top of and keeping the same API as react-loadable, this library enables you to load content that is visible on the screen.



GUIDE

Guide

So you've got your React app, you're bundling it with Webpack, and things are going smooth. But then one day you notice your app's bundle is getting so big that it's slowing things down.

It's time to start code-splitting your app!

A single giant bundle vs multiple smaller bundles

Code-splitting is the process of taking one large bundle containing your entire app, and splitting them up into multiple smaller bundles which contain separate parts of your app.

This might seem difficult to do, but tools like Webpack have this built in, and React Loadable is designed to make it super simple.

Route-based splitting vs. Component-based splitting

A common piece of advice you will see is to break your app into separate routes and load each one asynchronously. This seems to work well enough for many apps– as a user, clicking a link and waiting for a page to load is a familiar experience on the web.

But we can do better than that.

Using most routing tools for React, a route is simply a component. There's nothing particularly special about them (Sorry Ryan and Michael– you're what's special). So what if we optimized for splitting around components instead of routes? What would that get us?

Route vs. component centric code splitting

As it turns out: Quite a lot. There are many more places than just routes where you can pretty easily split apart your app. Modals, tabs, and many more UI components hide content until the user has done something to reveal it.

Example: Maybe your app has a map buried inside of a tab component. Why would you load a massive mapping library for the parent route every time when the user may never go to that tab?

Not to mention all the places where you can defer loading content until higher priority content is finished loading. That component at the bottom of your page which loads a bunch of libraries: Why should that be loaded at the same time as the content at the top?

And because routes are just components, we can still easily code-split at the route level.

Introducing new code-splitting points in your app should be so easy that you don't think twice about it. It should be a matter of changing a few lines of code and everything else should be automated.

Introducing React Loadable

React Loadable is a small library that makes component-centric code splitting incredibly easy in React.

Loadable is a higher-order component (a function that creates a component) which lets you dynamically load any module before rendering it into your app.

Let's imagine two components, one that imports and renders another.

import Bar from './components/Bar';

class Foo extends React.Component {
  render() {
    return <Bar/>;
  }
}

Right now we're depending on Bar being imported synchronously via import, but we don't need it until we go to render it. So why don't we just defer that?

Using a dynamic import (a tc39 proposal currently at Stage 3) we can modify our component to load Bar asynchronously.

class MyComponent extends React.Component {
  state = {
    Bar: null
  };

  componentWillMount() {
    import('./components/Bar').then(Bar => {
      this.setState({ Bar: Bar.default });
    });
  }

  render() {
    let {Bar} = this.state;
    if (!Bar) {
      return <div>Loading...</div>;
    } else {
      return <Bar/>;
    };
  }
}

But that's a whole bunch of work, and it doesn't even handle a bunch of cases. What about when import() fails? What about server-side rendering?

Instead you can use Loadable to abstract away the problem.

import Loadable from 'react-loadable';

const LoadableBar = Loadable({
  loader: () => import('./components/Bar'),
  loading() {
    return <div>Loading...</div>
  }
});

class MyComponent extends React.Component {
  render() {
    return <LoadableBar/>;
  }
}

Automatic code-splitting on import()

When you use import() with Webpack 2+, it will automatically code-split for you with no additional configuration.

This means that you can easily experiment with new code splitting points just by switching to import() and using React Loadable. Figure out what performs best for your app.

Creating a great "Loading…" Component

Rendering a static "Loading…" doesn't communicate enough to the user. You also need to think about error states, timeouts, and making it a nice experience.

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

Loadable({
  loader: () => import('./WillFailToLoad'), // oh no!
  loading: Loading,
});

To make this all nice, your loading component receives a couple different props.

Loading error states

When your loader fails, your loading component will receive an error prop which will be an Error object (otherwise it will be null).

function Loading(props) {
  if (props.error) {
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else {
    return <div>Loading...</div>;
  }
}

Avoiding Flash Of Loading Component

Sometimes components load really quickly (<200ms) and the loading screen only quickly flashes on the screen.

A number of user studies have proven that this causes users to perceive things taking longer than they really have. If you don't show anything, users perceive it as being faster.

So your loading component will also get a pastDelay prop which will only be true once the component has taken longer to load than a set delay.

function Loading(props) {
  if (props.error) {
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

This delay defaults to 200ms but you can also customize the delay in Loadable.

Loadable({
  loader: () => import('./components/Bar'),
  loading: Loading,
  delay: 300, // 0.3 seconds
});

Timing out when the loader is taking too long

Sometimes network connections suck and never resolve or fail, they just hang there forever. This sucks for the user because they won't know if it should always take this long, or if they should try refreshing.

The loading component will receive a timedOut prop which will be set to true when the loader has timed out.

function Loading(props) {
  if (props.error) {
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.timedOut) {
    return <div>Taking a long time... <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

However, this feature is disabled by default. To turn it on, you can pass a timeout option to Loadable.

Loadable({
  loader: () => import('./components/Bar'),
  loading: Loading,
  timeout: 10000, // 10 seconds
});

Customizing rendering

By default Loadable will render the default export of the returned module. If you want to customize this behavior you can use the render option.

Loadable({
  loader: () => import('./my-component'),
  render(loaded, props) {
    let Component = loaded.namedExport;
    return <Component {...props}/>;
  }
});

Loading multiple resources

Technically you can do whatever you want within loader() as long as it returns a promise and you're able to render something. But writing it out can be a bit annoying.

To make it easier to load multiple resources in parallel, you can use Loadable.Map.

Loadable.Map({
  loader: {
    Bar: () => import('./Bar'),
    i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
  },
  render(loaded, props) {
    let Bar = loaded.Bar.default;
    let i18n = loaded.i18n;
    return <Bar {...props} i18n={i18n}/>;
  },
});

When using Loadable.Map the render() method is required. It will be passed a loaded param which will be an object matching the shape of your loader.

Preloading

As an optimization, you can also decide to preload a component before it gets rendered.

For example, if you need to load a new component when a button gets pressed, you could start preloading the component when the user hovers over the button.

The component created by Loadable exposes a static preload method which does exactly this.

const LoadableBar = Loadable({
  loader: () => import('./Bar'),
  loading: Loading,
});

class MyComponent extends React.Component {
  state = { showBar: false };

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

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

  render() {
    return (
      <div>
        <button
          onClick={this.onClick}
          onMouseOver={this.onMouseOver}>
          Show Bar
        </button>
        {this.state.showBar && <LoadableBar/>}
      </div>
    )
  }
}



SERVER SIDE RENDERING

Server-Side Rendering

When you go to render all these dynamically loaded components, what you'll get is a whole bunch of loading screens.

This really sucks, but the good news is that React Loadable is designed to make server-side rendering work as if nothing is being loaded dynamically.

Here's our starting server using Express.

import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './components/App';

const app = express();

app.get('/', (req, res) => {
  res.send(`
    <!doctype html>
    <html lang="en">
      <head>...</head>
      <body>
        <div id="app">${ReactDOMServer.renderToString(<App/>)}</div>
        <script src="/dist/main.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000, () => {
  console.log('Running on http://localhost:3000/');
});

Preloading all your loadable components on the server

The first step to rendering the correct content from the server is to make sure that all of your loadable components are already loaded when you go to render them.

To do this, you can use the Loadable.preloadAll method. It returns a promise that will resolve when all your loadable components are ready.

Loadable.preloadAll().then(() => {
  app.listen(3000, () => {
    console.log('Running on http://localhost:3000/');
  });
});

Picking up a server-side rendered app on the client

This is where things get a little bit tricky. So let's prepare ourselves little bit.

In order for us to pick up what was rendered from the server we need to have all the same code that was used to render on the server.

To do this, we first need our loadable components telling us which modules they are rendering.

Declaring which modules are being loaded

There are two options in Loadable and Loadable.Map which are used to tell us which modules our component is trying to load: opts.modules and opts.webpack.

Loadable({
  loader: () => import('./Bar'),
  modules: ['./Bar'],
  webpack: () => [require.resolveWeak('./Bar')],
});

But don't worry too much about these options. React Loadable includes a Babel plugin to add them for you.

Just add the react-loadable/babel plugin to your Babel config:

{
  "plugins": [
    "react-loadable/babel"
  ]
}

Now these options will automatically be provided.

Finding out which dynamic modules were rendered

Next we need to find out which modules were actually rendered when a request comes in.

For this, there is Loadable.Capture component which can be used to collect all the modules that were rendered.

import Loadable from 'react-loadable';

app.get('/', (req, res) => {
  let modules = [];

  let html = ReactDOMServer.renderToString(
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
      <App/>
    </Loadable.Capture>
  );

  console.log(modules);

  res.send(`...${html}...`);
});

Mapping loaded modules to bundles

In order to make sure that the client loads all the modules that were rendered server-side, we'll need to map them to the bundles that Webpack created.

This comes in two parts.

First we need Webpack to tell us which bundles each module lives inside. For this there is the React Loadable Webpack plugin.

Import the ReactLoadablePlugin from react-loadable/webpack and include it in your webpack config. Pass it a filename for where to store the JSON data about our bundles.

// webpack.config.js
import { ReactLoadablePlugin } from 'react-loadable/webpack';

export default {
  plugins: [
    new ReactLoadablePlugin({
      filename: './dist/react-loadable.json',
    }),
  ],
};

Then we'll go back to our server and use this data to convert our modules to bundles.

To convert from modules to bundles, import the getBundles method from react-loadable/webpack and the data from Webpack.

import Loadable from 'react-loadable';
import { getBundles } from 'react-loadable/webpack'
import stats from './dist/react-loadable.json';

app.get('/', (req, res) => {
  let modules = [];

  let html = ReactDOMServer.renderToString(
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
      <App/>
    </Loadable.Capture>
  );

  let bundles = getBundles(stats, modules);

  // ...
});

We can then render these bundles into <script> tags in our HTML.

It is important that the bundles are included before the main bundle, so that they can be loaded by the browser prior to the app rendering.

However, as the Webpack manifest (including the logic for parsing bundles) lives in the main bundle, it will need to be extracted into its own chunk.

This is easy to do with the CommonsChunkPlugin

// webpack.config.js
export default {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    })
  ]
}

Notice: As of Webpack 4 the CommonsChunkPlugin has been removed and the manifest doesn't need to be extracted anymore.

let bundles = getBundles(stats, modules);

res.send(`
  <!doctype html>
  <html lang="en">
    <head>...</head>
    <body>
      <div id="app">${html}</div>
      <script src="/dist/manifest.js"></script>
      ${bundles.map(bundle => {
        return `<script src="/dist/${bundle.file}"></script>`
        // alternatively if you are using publicPath option in webpack config
        // you can use the publicPath value from bundle, e.g:
        // return `<script src="${bundle.publicPath}"></script>`
      }).join('\n')}
      <script src="/dist/main.js"></script>
    </body>
  </html>
`);

Preloading ready loadable components on the client

We can use the Loadable.preloadReady() method on the client to preload the loadable components that were included on the page.

Like Loadable.preloadAll(), it returns a promise, which on resolution means that we can hydrate our app.

// src/entry.js
import React from 'react';
import ReactDOM from 'react-dom';
import Loadable from 'react-loadable';
import App from './components/App';

Loadable.preloadReady().then(() => {
  ReactDOM.hydrate(<App/>, document.getElementById('app'));
});

Now server-side rendering should work perfectly!



API DOCS

API Docs

Loadable

A higher-order component for dynamically loading a module before rendering it, a loading component is rendered while the module is unavailable.

const LoadableComponent = Loadable({
  loader: () => import('./Bar'),
  loading: Loading,
  delay: 200,
  timeout: 10000,
});

This returns a LoadableComponent.

Loadable.Map

A higher-order component that allows you to load multiple resources in parallel.

Loadable.Map's opts.loader accepts an object of functions, and needs a opts.render method.

Loadable.Map({
  loader: {
    Bar: () => import('./Bar'),
    i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
  },
  render(loaded, props) {
    let Bar = loaded.Bar.default;
    let i18n = loaded.i18n;
    return <Bar {...props} i18n={i18n}/>;
  }
});

When using Loadable.Map the render() method's loaded param will be an object with the same shape as your loader.

Loadable and Loadable.Map Options

opts.loader

A function returning a promise that loads your module.

Loadable({
  loader: () => import('./Bar'),
});

When using with Loadable.Map this accepts an object of these types of functions.

Loadable.Map({
  loader: {
    Bar: () => import('./Bar'),
    i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
  },
});

When using with Loadable.Map you'll also need to pass a opts.render function.

opts.loading

A LoadingComponent that renders while a module is loading or when it errors.

Loadable({
  loading: LoadingComponent,
});

This option is required, if you don't want to render anything, return null.

Loadable({
  loading: () => null,
});

opts.delay

Time to wait (in milliseconds) before passing props.pastDelay to your loading component. This defaults to 200.

Loadable({
  delay: 200
});

Read more about delays.

opts.timeout

Time to wait (in milliseconds) before passing props.timedOut to your loading component. This is turned off by default.

Loadable({
  timeout: 10000
});

Read more about timeouts.

opts.render

A function to customize the rendering of loaded modules.

Receives loaded which is the resolved value of opts.loader and props which are the props passed to the LoadableComponent.

Loadable({
  render(loaded, props) {
    let Component = loaded.default;
    return <Component {...props}/>;
  }
});

opts.webpack

An optional function which returns an array of Webpack module ids which you can get with require.resolveWeak.

Loadable({
  loader: () => import('./Foo'),
  webpack: () => [require.resolveWeak('./Foo')],
});

This option can be automated with the Babel Plugin.

opts.modules

An optional array with module paths for your imports.

Loadable({
  loader: () => import('./my-component'),
  modules: ['./my-component'],
});

This option can be automated with the Babel Plugin.

LoadableComponent

This is the component returned by Loadable and Loadable.Map.

const LoadableComponent = Loadable({
  // ...
});

Props passed to this component will be passed straight through to the dynamically loaded component via opts.render.

LoadableComponent.preload()

This is a static method on LoadableComponent which can be used to load the component ahead of time.

const LoadableComponent = Loadable({...});

LoadableComponent.preload();

This returns a promise, but you should avoid waiting for that promise to resolve to update your UI. In most cases it creates a bad user experience.

Read more about preloading.

LoadingComponent

This is the component you pass to opts.loading.

function LoadingComponent(props) {
  if (props.error) {
    // When the loader has errored
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.timedOut) {
    // When the loader has taken longer than the timeout
    return <div>Taking a long time... <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.pastDelay) {
    // When the loader has taken longer than the delay
    return <div>Loading...</div>;
  } else {
    // When the loader has just started
    return null;
  }
}

Loadable({
  loading: LoadingComponent,
});

Read more about loading components

props.error

An Error object passed to LoadingComponent when the loader has failed. When there is no error, null is passed.

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

Read more about errors.

props.retry

A function prop passed to LoadingComponent when the loader has failed, used to retry loading the component.

function LoadingComponent(props) {
  if (props.error) {
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else {
    return <div>Loading...</div>;
  }
}

Read more about errors.

props.timedOut

A boolean prop passed to LoadingComponent after a set timeout.

function LoadingComponent(props) {
  if (props.timedOut) {
    return <div>Taking a long time...</div>;
  } else {
    return <div>Loading...</div>;
  }
}

Read more about timeouts.

props.pastDelay

A boolean prop passed to LoadingComponent after a set delay.

function LoadingComponent(props) {
  if (props.pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

Read more about delays.

Loadable.preloadAll()

This will call all of the LoadableComponent.preload methods recursively until they are all resolved. Allowing you to preload all of your dynamic modules in environments like the server.

Loadable.preloadAll().then(() => {
  app.listen(3000, () => {
    console.log('Running on http://localhost:3000/');
  });
});

It's important to note that this requires that you declare all of your loadable components when modules are initialized rather than when your app is being rendered.

Good:

// During module initialization...
const LoadableComponent = Loadable({...});

class MyComponent extends React.Component {
  componentDidMount() {
    // ...
  }
}

Bad:

// ...

class MyComponent extends React.Component {
  componentDidMount() {
    // During app render...
    const LoadableComponent = Loadable({...});
  }
}

Note: Loadable.preloadAll() will not work if you have more than one copy of react-loadable in your app.

Read more about preloading on the server.

Loadable.preloadReady()

Check for modules that are already loaded in the browser and call the matching LoadableComponent.preload methods.

Loadable.preloadReady().then(() => {
  ReactDOM.hydrate(<App/>, document.getElementById('app'));
});

Read more about preloading on the client.

Loadable.Capture

A component for reporting which modules were rendered.

Accepts a report prop which is called for every moduleName that is rendered via React Loadable.

let modules = [];

let html = ReactDOMServer.renderToString(
  <Loadable.Capture report={moduleName => modules.push(moduleName)}>
    <App/>
  </Loadable.Capture>
);

console.log(modules);

Read more about capturing rendered modules.

Babel Plugin

Providing opts.webpack and opts.modules for every loadable component is a lot of manual work to remember to do.

Instead you can add the Babel plugin to your config and it will automate it for you:

{
  "plugins": ["react-loadable/babel"]
}

Input

import Loadable from 'react-loadable';

const LoadableMyComponent = Loadable({
  loader: () => import('./MyComponent'),
});

const LoadableComponents = Loadable.Map({
  loader: {
    One: () => import('./One'),
    Two: () => import('./Two'),
  },
});

Output

import Loadable from 'react-loadable';
import path from 'path';

const LoadableMyComponent = Loadable({
  loader: () => import('./MyComponent'),
  webpack: () => [require.resolveWeak('./MyComponent')],
  modules: [path.join(__dirname, './MyComponent')],
});

const LoadableComponents = Loadable.Map({
  loader: {
    One: () => import('./One'),
    Two: () => import('./Two'),
  },
  webpack: () => [require.resolveWeak('./One'), require.resolveWeak('./Two')],
  modules: [path.join(__dirname, './One'), path.join(__dirname, './Two')],
});

Read more about declaring modules.

Webpack Plugin

In order to send the right bundles down when rendering server-side, you'll need the React Loadable Webpack plugin  to provide you with a mapping of modules to bundles.

// webpack.config.js
import { ReactLoadablePlugin } from 'react-loadable/webpack';

export default {
  plugins: [
    new ReactLoadablePlugin({
      filename: './dist/react-loadable.json',
    }),
  ],
};

This will create a file (opts.filename) which you can import to map modules to bundles.

Read more about mapping modules to bundles.

getBundles

A method exported by react-loadable/webpack for converting modules to bundles.

import { getBundles } from 'react-loadable/webpack';

let bundles = getBundles(stats, modules);

Read more about mapping modules to bundles.



FAQ

FAQ

How do I avoid repetition?

Specifying the same loading component or delay every time you use Loadable() gets repetitive fast. Instead you can wrap Loadable with your own Higher-Order Component (HOC) to set default options.

import Loadable from 'react-loadable';
import Loading from './my-loading-component';

export default function MyLoadable(opts) {
  return Loadable(Object.assign({
    loading: Loading,
    delay: 200,
    timeout: 10000,
  }, opts));
};

Then you can just specify a loader when you go to use it.

import MyLoadable from './MyLoadable';

const LoadableMyComponent = MyLoadable({
  loader: () => import('./MyComponent'),
});

export default class App extends React.Component {
  render() {
    return <LoadableMyComponent/>;
  }
}

Unfortunately at the moment using wrapped Loadable breaks react-loadable/babel so in such case you have to add required properties (modules, webpack) manually.

import MyLoadable from './MyLoadable';

const LoadableMyComponent = MyLoadable({
  loader: () => import('./MyComponent'),
  modules: ['./MyComponent'],
  webpack: () => [require.resolveWeak('./MyComponent')],
});

export default class App extends React.Component {
  render() {
    return <LoadableMyComponent/>;
  }
}

How do I handle other styles .css or sourcemaps .map with server-side rendering?

When you call getBundles, it may return file types other than JavaScript depending on your Webpack configuration.

To handle this, you should manually filter down to the file extensions that you care about:

let bundles = getBundles(stats, modules);

let styles = bundles.filter(bundle => bundle.file.endsWith('.css'));
let scripts = bundles.filter(bundle => bundle.file.endsWith('.js'));

res.send(`
  <!doctype html>
  <html lang="en">
    <head>
      ...
      ${styles.map(style => {
        return `<link href="/dist/${style.file}" rel="stylesheet"/>`
      }).join('\n')}
    </head>
    <body>
      <div id="app">${html}</div>
      <script src="/dist/main.js"></script>
      ${scripts.map(script => {
        return `<script src="/dist/${script.file}"></script>`
      }).join('\n')}
    </body>
  </html>
`);
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文