A higher order component for loading components with dynamic imports. This is a direct port from the original react-loadable project to Meteor.
Meteor has a few different requirements for making this all work, and some incompatibilities with the original react-loadable when it comes to server side rendering (SSR). This fork addresses those issues, while also streamlining some of the APIs. The examples in this readme have also been updated for Meteor.
meteor add npdev:react-loadableimport{Loadable}from'meteor/npdev:react-loadable';importLoadingfrom'./my-loading-component';constLoadableComponent=Loadable({loader: ()=>import('./my-component'),loading: Loading,});exportdefaultclassAppextendsReact.Component{render(){return<LoadableComponent/>;}}- "I'm obsessed with this right now: CRA with React Router v4 and react-loadable. Free code splitting, this is so easy."
- "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."
- "We've reduced our entry chunk by a lot & reduced initial load time by ~50%!"
- "React-loadable is killer! We've decreased our load size by over 50kb with only 2 files! Can't wait to see how much lower it will go."
- AdHawk / Flooring Stores
- Akutbolig.dk
- Analog.Cafe
- Ambrosus
- Appbase.io
- Atlassian
- BBC News
- Blytzpay
- ClearTax
- Cloudflare
- Chibaki
- Curio
- Delivery.com
- Doctor.com
- Dollar Shave Club
- Dresez
- Evidation Health
- Flyhomes
- Gogo
- Gofore
- Graana
- Localie
- MediaTek MCS-Lite
- NiYO Solutions Inc.
- NP Dev
- Officepulse
- Perx
- Pixstori
- Plottu
- Render
- Shift
- Snipit
- Spectrum.chat
- Talentpair
- Tinder
- Unsplash
- Wave
- WUZZUF
If your company or project is using React Loadable, please open a PR and add yourself to this list (in alphabetical order please)
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.
So you've got your React app, you're bundling it with Meteor, 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 Meteor have this built in, and React Loadable is designed to make it super simple.
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.
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.
importBarfrom'./components/Bar';classFooextendsReact.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.
classMyComponentextendsReact.Component{state={Bar: null};componentDidMount(){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'meteor/npdev:react-loadable';constLoadableBar=Loadable({loader: ()=>import('./components/Bar'),loading(){return<div>Loading...</div>}});classMyComponentextendsReact.Component{render(){return<LoadableBar/>;}}When you use import() with Meteor 1.5+, 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.
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.
functionLoading(){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.
When your loader fails, your loading component
will receive an error prop which will be an Error object (otherwise it
will be null).
functionLoading(props){if(props.error){return<div>Error!</div>;}else{return<div>Loading...</div>;}}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.
functionLoading(props){if(props.error){return<div>Error!</div>;}elseif(props.pastDelay){return<div>Loading...</div>;}else{returnnull;}}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});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.
functionLoading(props){if(props.error){return<div>Error!</div>;}elseif(props.timedOut){return<div>Taking a long time...</div>;}elseif(props.pastDelay){return<div>Loading...</div>;}else{returnnull;}}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});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){letComponent=loaded.namedExport;return<Component{...props}/>;}});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
LoadableMap.
LoadableMap({loader: {Bar: ()=>import('./Bar'),i18n: ()=>fetch('./i18n/bar.json').then(res=>res.json()),},render(loaded,props){letBar=loaded.Bar.default;leti18n=loaded.i18n;return<Bar{...props}i18n={i18n}/>;},});When using LoadableMap the render() method is required. It
will be passed a loaded param which will be an object matching the shape of
your loader.
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.
constLoadableBar=Loadable({loader: ()=>import('./Bar'),loading: Loading,});classMyComponentextendsReact.Component{state={showBar: false};onClick=()=>{this.setState({showBar: true});};onMouseOver=()=>{LoadableBar.preload();};render(){return(<div><buttononClick={this.onClick}onMouseOver={this.onMouseOver}>
Show Bar
</button>{this.state.showBar&&<LoadableBar/>}</div>)}}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-rendering using Meteor.
importReactfrom'react'import{renderToString}from'react-dom/server'import{onPageLoad}from'meteor/server-render'importAppfrom'/imports/App'onPageLoad(sink=>{sink.renderIntoElementById('root',renderToString(<App/>)})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 preloadLoadables
method. It returns a promise that will resolve when all your loadable
components are ready.
preloadLoadables().then(()=>{onPageLoad(sink=>{sink.renderIntoElementById('root',renderToString(<App/>)})});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.
There is one option in Loadable and
LoadableMap which is used to tell us which modules our
component is trying to load: opts.meteor.
Loadable({loader: ()=>import('./Bar'),meteor: ()=>[require.resolve('./Bar')]});But don't worry too much about this option. React Loadable includes a Babel plugin to add it for you.
Install the npdev version of the react-loadable babel plugin from npm:
$ meteor npm i -D npdev-react-loadable-babel
And add the npdev-react-loadable-babel plugin to your Babel config:
{
"plugins": [
"npdev-react-loadable-babel"
]
}Now this option will automatically be provided.
Next we need to find out which modules were actually rendered when a request comes in.
For this, there is LoadableCaptureProvider component which can
be used to collect all the modules that were rendered.
import{LoadableCaptureProvider,preloadAllLoadables}from'meteor/npdev:react-loadable'preloadAllLoadables().then(()=>{onPageLoad(sink=>{constloadableHandle={};consthtml=ReactDOMServer.renderToString(<LoadableCaptureProviderhandle={loadableHandle}><App/></LoadableCaptureProvider>);sink.renderIntoElementById('root',renderToString(app))console.log(modules);sink.appendToBody(loadableHandle.toScriptTag())})});We can use the preloadLoadables() method on the
client to preload the loadable components that were included on the page.
Like preloadAllLoadables() on the server, it returns
a promise, which on resolution means that we can hydrate our app.
// src/entry.jsimportReactfrom'react';importReactDOMfrom'react-dom';import{preloadLoadables}from'meteor/npdev:react-loadable'importAppfrom'./components/App';preloadLoadables().then(()=>{ReactDOM.hydrate(<App/>,document.getElementById('app'));});A higher-order component for dynamically loading a module before rendering it, a loading component is rendered while the module is unavailable.
constLoadableComponent=Loadable({loader: ()=>import('./Bar'),loading: Loading,delay: 200,timeout: 10000,});This returns a LoadableComponent.
A higher-order component that allows you to load multiple resources in parallel.
LoadableMap's opts.loader accepts an object of functions, and
needs a opts.render method.
LoadableMap({loader: {Bar: ()=>import('./Bar'),i18n: ()=>fetch('./i18n/bar.json').then(res=>res.json()),},render(loaded,props){letBar=loaded.Bar.default;leti18n=loaded.i18n;return<Bar{...props}i18n={i18n}/>;}});When using LoadableMap the render() method's loaded param will be an
object with the same shape as your loader. Note: A reference to LoadableMap
is available on Loadable.Map for backward compatibility and easy porting.
A function returning a promise that loads your module.
Loadable({loader: ()=>import('./Bar'),});When using with LoadableMap this accepts an object of these
types of functions.
LoadableMap({loader: {Bar: ()=>import('./Bar'),i18n: ()=>fetch('./i18n/bar.json').then(res=>res.json()),},});When using with LoadableMap you'll also need to pass a
opts.render function.
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,});Time to wait (in milliseconds) before passing
props.pastDelay to your loading
component. This defaults to 200.
Loadable({delay: 200});Time to wait (in milliseconds) before passing
props.timedOut to your loading component.
This is turned off by default.
Loadable({timeout: 10000});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){letComponent=loaded.default;return<Component{...props}/>;}});An optional function which returns an array of meteor module ids which you can
get with require.resolve.
Loadable({loader: ()=>import('./Foo'),meteor: ()=>[require.resolve('./Foo')],});This option can be automated with the Babel Plugin.
This is the component returned by Loadable and LoadableMap.
constLoadableComponent=Loadable({// ...});Props passed to this component will be passed straight through to the
dynamically loaded component via opts.render.
This is a static method on LoadableComponent which can
be used to load the component ahead of time.
constLoadableComponent=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.
This is the component you pass to opts.loading.
functionLoadingComponent(props){if(props.error){// When the loader has erroredreturn<div>Error!</div>;}elseif(props.timedOut){// When the loader has taken longer than the timeoutreturn<div>Taking a long time...</div>;}elseif(props.pastDelay){// When the loader has taken longer than the delayreturn<div>Loading...</div>;}else{// When the loader has just startedreturnnull;}}Loadable({loading: LoadingComponent,});Read more about loading components
An Error object passed to LoadingComponent when the
loader has failed. When there is no error, null is
passed.
functionLoadingComponent(props){if(props.error){return<div>Error!</div>;}else{return<div>Loading...</div>;}}A boolean prop passed to LoadingComponent after a set
timeout.
functionLoadingComponent(props){if(props.timedOut){return<div>Taking a long time...</div>;}else{return<div>Loading...</div>;}}A boolean prop passed to LoadingComponent after a set
delay.
functionLoadingComponent(props){if(props.pastDelay){return<div>Loading...</div>;}else{returnnull;}}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.
import{preloadAllLoadables}from'meteor/npdev:react-loadable'preloadAllLoadables().then(()=>onPageLoad(sink=>{// do server rendering});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...constLoadableComponent=Loadable({...});classMyComponentextendsReact.Component{componentDidMount(){// ...}}Bad:
// ...classMyComponentextendsReact.Component{componentDidMount(){// During app render...constLoadableComponent=Loadable({...});}}Note:
preloadLoadables()will not work if you have more than one copy ofreact-loadablein your app.
Read more about preloading on the server.
Check for modules that hav been rendered on the server and call the matching
LoadableComponent.preload methods.
preloadLoadables().then(()=>{ReactDOM.hydrate(<App/>,document.getElementById('app'));});Read more about preloading on the client.
A component for recording which modules were rendered in SSR.
Accepts a handle prop, and object which is extended with a list of
resolved module ids, and helpful methods; toEJON and toScriptTag.
import{LoadableCaptureProvider,preloadLoadables}from'meteor/npdev:react-loadable'preloadLoadables().then(()=>onPageLoad(sink=>{constloadableHandle={};consthtml=ReactDOMServer.renderToString(<LoadableCaptureProviderhandle={loadableHandle}><App/></LoadableCaptureProvider>);sink.renderIntoElementById('root',html)console.log(loadableHandle);sink.appendToBody(loadableHandle.toScriptTag());}));Read more about capturing rendered modules.
Providing opts.meteor for
every loadable component is a lot of manual work to remember to do.
Instead you can install the babel plugin from npm, and add the Babel plugin to your config and it will automate it for you:
$ meteor npm i -D npdev-react-loadable-babel
{
"plugins": ["npdev-react-loadable-babel"]
}Input
import{Loadable,LoadableMap}from'meteor/npdev:react-loadable';constLoadableMyComponent=Loadable({loader: ()=>import('./MyComponent'),});constLoadableComponents=LoadableMap({loader: {One: ()=>import('./One'),Two: ()=>import('./Two'),},});Output
import{Loadable,LoadableMap}from'meteor/npdev:react-loadable';importpathfrom'path';constLoadableMyComponent=Loadable({loader: ()=>import('./MyComponent'),meteor: ()=>[require.resolve('./MyComponent')]});constLoadableComponents=LoadableMap({loader: {One: ()=>import('./One'),Two: ()=>import('./Two'),},meteor: ()=>[require.resolve('./One'),require.resolve('./Two')]});Read more about declaring modules.
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'meteor/npdev:react-loadable';importLoadingfrom'./my-loading-component';exportdefaultfunctionMyLoadable(opts){returnLoadable(Object.assign({loading: Loading,delay: 200,timeout: 10000,},opts));};Then you can just specify a loader when you go to use it.
importMyLoadablefrom'./MyLoadable';constLoadableMyComponent=MyLoadable({loader: ()=>import('./MyComponent'),});exportdefaultclassAppextendsReact.Component{render(){return<LoadableMyComponent/>;}}Unfortunately at the moment using wrapped Loadable breaks npdev-react-loadable-babel so in such case you have to add required property (meteor) manually.
importMyLoadablefrom'./MyLoadable';constLoadableMyComponent=MyLoadable({loader: ()=>import('./MyComponent'),meteor: ()=>[require.resolve('./MyComponent')]});exportdefaultclassAppextendsReact.Component{render(){return<LoadableMyComponent/>;}}




