@7rulnik/react-loadable 中文文档教程
用于使用动态导入加载组件的高阶组件。
⚠️⚠️⚠️ Warning ⚠️⚠️⚠️
它是与 webpack@4 一起工作的 react-loadable 的一个分支! 如果您不了解发生了什么,则不应使用它。 您可以通过此更新跟踪原始 PR。
Install
yarn add @7rulnik/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:
- "I'm obsessed with this right now: CRA with React Router v4 and react-loadable. Free code splitting, this is so easy."
- "Webpack 2 upgrade & react-loadable; initial load from 1.1mb to 529kb in under 2 hours. Immense."
- "Oh hey - using loadable component I knocked 13K off my initial load. Easy win!"
- "Had a look and its awesome. shaved like 50kb off our main bundle."
- "I've got that server-side rendering + code splitting + PWA ServiceWorker caching setup done (thanks to react-loadable). Now our frontend is super fast."
- "Using react-loadable went from 221.28 KB → 115.76 KB @ main bundle. Fucking awesome and very simple API."
Users
如果你的公司或项目正在使用 React Loadable,请打开一个 PR 并添加 你自己到这个列表(请按字母顺序排列)
Also See:
react-loadable-visibility
- Building on top of and keeping the same API asreact-loadable
, this library enables you to load content that is visible on the screen.
Guide
所以你已经有了你的 React 应用程序,你将它与 Webpack 捆绑在一起,事情 顺利。 但是有一天你注意到你的应用程序包变得如此之大 它正在减慢速度。
是时候开始对您的应用程序进行代码拆分了!
代码拆分是一个大包的过程,其中包含你的整个 应用程序,并将它们分成多个较小的包,其中包含单独的 你的应用程序的一部分。
这似乎很难做到,但是像 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 });
});
}
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
属性,该属性将为 true
(否则 将为 false
)。
function Loading(props) {
if (props.error) {
return <div>Error!</div>;
} else {
return <div>Loading...</div>;
}
}
Avoiding Flash Of Loading Component
有时组件加载非常快(<200 毫秒)并且仅加载屏幕 快速闪烁在屏幕上。
多项用户研究证明,这会导致用户感知事物 比他们实际花费的时间更长。 如果你不展示任何东西,用户就会察觉 它更快。
所以你的加载组件也会得到一个 pastDelay
prop 只有当组件加载时间比一组时间长时才会为真 延迟。
function Loading(props) {
if (props.error) {
return <div>Error!</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
prop 当 loader
已超时。
function Loading(props) {
if (props.error) {
return <div>Error!</div>;
} else if (props.timedOut) {
return <div>Taking a long time...</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
当你去渲染所有这些动态加载的组件时,你会得到什么 是一大堆加载屏幕。
这真的很糟糕,但好消息是 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
Loadable
和 Loadable.Map
用于告诉我们哪些模块是我们的 组件正在尝试加载:opts.modules
和 opts.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
})
]
}
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>`
}).join('\n')}
<script src="/dist/main.js"></script>
</body>
</html>
`);
Preloading ready loadable components on the client
我们可以使用 Loadable.preloadReady()
方法 客户端预加载页面上包含的可加载组件。
像 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
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
这是 Loadable
和 Loadable.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!</div>;
} else if (props.timedOut) {
// When the loader has taken longer than the timeout
return <div>Taking a long time...</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;
}
}
Loading({
loading: LoadingComponent,
});
props.error
传递给 LoadingComponent
当 loader
失败。
function LoadingComponent(props) {
if (props.error) {
return <div>Error!</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
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: 10,
}, 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 所以在这种情况下你必须添加所需的属性(modules
,webpack
) 手动。
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>
`);
A higher order component for loading components with dynamic imports.
⚠️⚠️⚠️ Warning ⚠️⚠️⚠️
It's a fork of react-loadable that works with webpack@4! You should not use it if you don't understand what is going on. You can track original PR with this update.
Install
yarn add @7rulnik/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:
- "I'm obsessed with this right now: CRA with React Router v4 and react-loadable. Free code splitting, this is so easy."
- "Webpack 2 upgrade & react-loadable; initial load from 1.1mb to 529kb in under 2 hours. Immense."
- "Oh hey - using loadable component I knocked 13K off my initial load. Easy win!"
- "Had a look and its awesome. shaved like 50kb off our main bundle."
- "I've got that server-side rendering + code splitting + PWA ServiceWorker caching setup done ???? (thanks to react-loadable). Now our frontend is super fast."
- "Using react-loadable went from 221.28 KB → 115.76 KB @ main bundle. Fucking awesome and very simple API."
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 asreact-loadable
, this library enables you to load content that is visible on the screen.
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!
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?
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 });
});
}
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 true
(otherwise it will be false
).
function Loading(props) {
if (props.error) {
return <div>Error!</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!</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!</div>;
} else if (props.timedOut) {
return <div>Taking a long time...</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
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
})
]
}
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>`
}).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
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
});
opts.timeout
Time to wait (in milliseconds) before passing props.timedOut
to your loading
component. This is turned off by default.
Loadable({
timeout: 10000
});
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.
LoadingComponent
This is the component you pass to opts.loading
.
function LoadingComponent(props) {
if (props.error) {
// When the loader has errored
return <div>Error!</div>;
} else if (props.timedOut) {
// When the loader has taken longer than the timeout
return <div>Taking a long time...</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;
}
}
Loading({
loading: LoadingComponent,
});
Read more about loading components
props.error
A boolean prop passed to LoadingComponent
when the loader
has failed.
function LoadingComponent(props) {
if (props.error) {
return <div>Error!</div>;
} else {
return <div>Loading...</div>;
}
}
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>;
}
}
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;
}
}
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 ofreact-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
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: 10,
}, 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>
`);