This article introduces how to implement a high-order asynchronous load component for React by using the act-loadable package

1. First of all, how do we imagine the React asynchronous loading component to be entered and which apis are exposed?

// Import * as React from'react';
import ReactDOM from 'react-dom';
import Loadable from '@component/test/Loadable';
import Loading from '@component/test/loading';
const ComponentA = Loadable({
    loader: () => import(
        /* webpackChunkName: 'componentA'* /'@component/test/componentA.js'), loading: loading, // Loading component delay: 1000, // loading component delay: 1000, // loading component timeout: 1000, // loading component timeout}) component.preload (); // const ComponentB = Loadable({loader: () => import(/* webpackChunkName:'componentB'* /'@component/test/componentB.js'), loading: loading, loading components}) loadable.preloadAll ().then(() => {//}).catch(err => {//}); // Preload all asynchronous components const App = (props) => {const [isDisplay,setIsDisplay] = React.useState(false);
    if(isDisplay){
        return <React.Fragment>
            <ComponentA />
            <ComponentB />
        </React.Fragment> 
    }else{
        return <input type='button' value='am I' onClick={()=>{setIsDisplay(true)}}/>
    }
}

ReactDOM.render(<App />, document.getElementById('app'));
Copy the code
// Loading component import * as React from'react';

export default (props) => {
    const {error, pastDelay, isLoading, timedOut, retry} = props;
    if (props.error) {
        return <div>Error! <button onClick={ retry }>Retry</button></div>;
      } else if (timedOut) {
        return <div>Taking a long time... <button onClick={ retry }>Retry</button></div>;
      } else if (props.pastDelay) {
        return<div>Loading... </div>; }else {
        returnnull; }}Copy the code

As you can see from the examples, we need to enter loaded, loading, delay, timeout, and expose both the single preloading API and all the preloading API. Now let’s try to implement the higher-order component of Loadable step by step

2. Component implementation process

The entire Loaded function looks like this

// Collect all components that need to be loaded asynchronously for preloading const ALL_INITIALIZERS = [];function Loadable(opts){
    returncreateLoadableComponent(load, opts); } // Static method preloads all components loadable.preloadAll =function(){

}
Copy the code

Next, implement createLoadableComponent and the Load function

Preload a single asynchronous componentfunction load(loader){
    let promise = loader();
    let state = {
        loading: true,
        loaded: null,
        error: null,
    }
    state.promise = promise.then(loaded => {
        state.loading = false;
        state.loaded = loaded;
        return loaded;
    }).catch(err => {
        state.loading = false;
        state.error = err;
        throw err;
    })
    returnstate; } // Create asynchronously loaded higher-order componentsfunction createLoadableComponent(loadFn, options){
    if(! options.loading) { throw new Error("react-loadable requires a `loading` component");
    }
    let opts = Object.assign({
        loader: null,
        loading: null,
        delay: 200,
        timeout: null,
    }, options);

    let res = null;

    function init() {if(! res){ res = loadFn(options.loader);return res.promise;
        }
    }

    ALL_INITIALIZERS.push(init);

    return class LoadableComponent extends React{}
}
Copy the code

CreateLoadableComponent’s main functions include merging default configurations, pushing asynchronous components into preloaded arrays, and returning LoadableComponent components. The load function loads a single component and returns the initial load status of that component

Next we implement the core LoadableComponent component

class LoadableComponent extends React.Component{ constructor(props){ super(props); // Call init method to download asynchronous component init() before component initialization; this.state = { error: res.error, postDelay:false,
                timedOut: false,
                loading: res.loading,
                loaded: res.loaded
            }
            this._delay = null;
            this._timeout = null;
        }
        componentWillMountThis. _mounted = (){// Set the switch to ensure that asynchronous components are not re-requested more than oncetrue;
            this._loadModule();
        }

        _loadModule() {if(! res.loading)return;
            if(typeof opts.delay === 'number') {if(opts.delay === 0){
                    this.setState({pastDelay: true});
                }else{
                    this._delay = setTimeout(()=>{
                        this.setState({pastDelay: true});
                    }, opts.delay)
                }
            }

            if(typeof opts.timeout === 'number'){
                this._timeout = setTimeout(()=>{
                    this.setState({timedOut: true});
                }, opts.timeout)
            }

            let update = () => {
                if(! this._mounted)return; this.setState({ error: res.error, loaded: res.loaded, loading: res.loading, }); } // Receive the download results of the asynchronous component and retrysetState to render res.promise.then(()=>{update()}).catch(err =>{update()})} // reload the asynchronous componentretry(){
            this.setState({
                error: null,
                timedOut: false,
                loading: false}); res = loadFn(opts.loader); this._loadModule(); } // Static method single component preloaded staticpreload(){
            init()
        }


        componentWillUnmount(){
            this._mounted = false;
            clearTimeout(this._delay);
            clearTimeout(this._timeout);
        }

        render(){
            const {loading, error, pastDelay, timedOut, loaded} = this.state;
            if(loading | | error) {/ / asynchronous components has not been downloaded when rendering loading componentsreturn React.createElement(opts.loading, {
                    isLoading: loading,
                    pastDelay: pastDelay,
                    timedOut: timedOut,
                    error: error,
                    retry: this.retry.bind(this),
                })
            }else ifReact.createElement {// why not use react. createElement?return opts.render(loaded, this.props);
            }else{
                returnnull; }}}Copy the code

As you can see, init is called to start the download of the asynchronous component initially, and the pending result of the asynchronous component is received in the _loadModule method. After the asynchronous component is downloaded, setState is restarted to start render

Another detail is that the asynchronous component does not directly launch React. CreateElement to render, but opts.render. This is because webPack generates a separate chunk of the asynchronous component that exposes an object, and its default is the corresponding component

To achieve the following

function resolve(obj) {
    return obj && obj.__esModule ? obj.default : obj;
}
  
function render(loaded, props) {
    return React.createElement(resolve(loaded), props);
}
Copy the code

Finally, all the preloading methods are realized

Loadable.preloadAll = function() {let promises = [];
    while(initializers.length){
        const init = initializers.pop();
        promises.push(init())
    }
    return Promise.all(promises);
}
Copy the code

The entire code is implemented as follows

const React = require("react"); // Collect all components that need to be loaded asynchronously Preload a single asynchronous componentfunction load(loader){
    let promise = loader();
    let state = {
        loading: true,
        loaded: null,
        error: null,
    }
    state.promise = promise.then(loaded => {
        state.loading = false;
        state.loaded = loaded;
        return loaded;
    }).catch(err => {
        state.loading = false;
        state.error = err;
        throw err;
    })
    return state;
}

function resolve(obj) {
    return obj && obj.__esModule ? obj.default : obj;
}
  
function render(loaded, props) {
    returnReact.createElement(resolve(loaded), props); } // Create asynchronously loaded higher-order componentsfunction createLoadableComponent(loadFn, options){
    if(! options.loading) { throw new Error("react-loadable requires a `loading` component");
    }
    let opts = Object.assign({
        loader: null,
        loading: null,
        delay: 200,
        timeout: null,
        render,
    }, options);

    let res = null;

    function init() {if(! res){ res = loadFn(options.loader);return res.promise;
        }
    }

    ALL_INITIALIZERS.push(init);

    class LoadableComponent extends React.Component{
        constructor(props){
            super(props);
            init();
            this.state = {
                error: res.error,
                postDelay: false,
                timedOut: false,
                loading: res.loading,
                loaded: res.loaded
            }
            this._delay = null;
            this._timeout = null;
        }

        

        componentWillMount(){
            this._mounted = true;
            this._loadModule();
        }

        _loadModule() {if(! res.loading)return;
            if(typeof opts.delay === 'number') {if(opts.delay === 0){
                    this.setState({pastDelay: true});
                }else{
                    this._delay = setTimeout(()=>{
                        this.setState({pastDelay: true});
                    }, opts.delay)
                }
            }

            if(typeof opts.timeout === 'number'){
                this._timeout = setTimeout(()=>{
                    this.setState({timedOut: true});
                }, opts.timeout)
            }

            let update = () => {
                if(! this._mounted)return; this.setState({ error: res.error, loaded: res.loaded, loading: res.loading, }); } res. Promise. Then (() = > {the update ()}). The catch (err = > {the update ()})} / / reload asynchronous componentsretry(){
            this.setState({
                error: null,
                timedOut: false,
                loading: false}); res = loadFn(opts.loader); this._loadModule(); } staticpreload(){
            init()
        }


        componentWillUnmount(){
            this._mounted = false;
            clearTimeout(this._delay);
            clearTimeout(this._timeout);
        }

        render(){
            const {loading, error, pastDelay, timedOut, loaded} = this.state;
            if(loading || error){
                return React.createElement(opts.loading, {
                    isLoading: loading,
                    pastDelay: pastDelay,
                    timedOut: timedOut,
                    error: error,
                    retry: this.retry.bind(this),
                })
            }else if(loaded){
                return opts.render(loaded, this.props);
            }else{
                returnnull; }}}return LoadableComponent;
}

function Loadable(opts){
    return createLoadableComponent(load, opts);
}

function flushInitializers(initializers){
    
    
}
Loadable.preloadAll = function() {let promises = [];
    while(initializers.length){
        const init = initializers.pop();
        promises.push(init())
    }
    return Promise.all(promises);
}

export default Loadable;
Copy the code

Github has been unable to open recently, so I have to put the source code on the cloud