This is the fourth day of my participation in the August More text Challenge. For details, see:August is more challenging

A higher order component for loading components with dynamic imports.

In the last part, we have introduced the React Loadable source code in the “CSR” client rendering scene, this part we continue to talk about the source code in the SSR scene, and read the SSR part with the questions raised in the last part:

  1. The init function for each dynamically loaded component is pushed into the ALL_INITIALIZERS queue. When are ALL_INITIALIZERS invoked?
  2. We only inserted into the ALL_INITIALIZERS queue, but it seems that dynamic loading works without ALL_INITIALIZERS all the time. What does it do?

SSR scenario

SSR, or server rendering. We add a layer of servers on top of CSR.

❓❓❓ Why should React Loadable use ALL_INITIALIZERS in SSR scenarios?

Before we talk about the usefulness of ALL_INITIALIZERS, let’s consider: What’s wrong with the example code in the SSR scenario?

const Footer = Loadable({
  loader: () = > import('@service/Footer')
  loading: Loading,
})

export const App = () = > {
  return (
    <div>
      <Footer />
    </div>)}Copy the code
  • First, we need to render the App as a string. It is synchronized during rendering, so the Footer component is destined to have no real DOM yet.

    Why does Footer have no real DOM when synchronizing state?

    When synchronizing status, Loadable returns only a LoadableComponent component. The component @service/Footer that needs to be loaded dynamically is introduced dynamically, and the component itself needs to be retrieved in the promise callback. So when React to renderToString, Footer does not have a real DOM, only loading component DOM.

  • Also, if nothing is done in this case, the Footer part of the HTML returning to the front end will of course have a loading effect, which is obviously not what we want.

There are problems with SSR using the same dynamic loading approach as CSR, so these issues need to be addressed.

Let’s think: in the SSR scenario, is it ok to render the component in advance, or wait for the component to be rendered and then return to the front end?

In fact, this sentence contains two scenarios:

  1. When requesting the corresponding lazy loaded component, wait until the real DOM has rendered before returning the component
  2. The component is rendered when the service starts

There are actually two problems with the first solution:

  • If the corresponding component is requested and then waits to be loaded, there must be a waiting time.
  • Second, if n users visit pages that all need this component, then each user has to wait.

So, we ruled out the first option.

React Loadable also uses the second solution 😬

So how does it work?

The workings of ALL_INITIALIZERS and READY_INITIALIZERS will also be revealed here.

Let’s change the client code first:

window.main = () = > {
  Loadable.preloadReady().then(() = > {
    ReactDOM.hydrate(<App/>.document.getElementById('app'));
  });
};
Copy the code

Originally we would have simply rendered the ReactDOM. Hydrate App, but now we’ve done two things:

  1. Wrap it in the main function

    Because many components are loaded dynamically with React Loadable, you must wait for all dynamically loaded components to be loaded before rendering can start, otherwise you will get an error: ‘no xx component found’.

  2. Wait for loadable.preloadReady () to complete before rendering

    We chose the second one based on the above two SSR rendering schemes

The components need to be loaded when the service starts, so preloadReady is for that purpose. Let’s look at the code:

Loadable.preloadReady = () = > {
  return new Promise((resolve, reject) = > {
    flushInitializers(READY_INITIALIZERS).then(resolve, resolve); 
  });
};
Copy the code
  • The use of the flushInitializers method is defined in the following section: Iterate through the incoming task queue and batch execution through promise. all.
  • You can see that preloadReady returns a Promise and passes in the READY_INITIALIZERS queue.
  • In the end, whether it succeeds or fails, it will resolve.

We analyze this from three points:

  1. What does flushInitializers do?

    Take a look at the source code for flushInitializers at 👇

    function flushInitializers(initializers) { 
      let promises = [];
    
      while (initializers.length) {
        let init = initializers.pop();
        promises.push(init());
      }
    
      return Promise.all(promises).then(() = > {
        if (initializers.length) {
          returnflushInitializers(initializers); }}); }Copy the code
    • The entry initializers are queues

    • Walk through the queue and add to the promises array in turn

    • The array is executed through promise.all, and finally resolve

The problem is that until promise.all () is executed, it looks like initializers must be empty. So why is initializers putin.length still in the promise.all () callback?

Let’s go to 🌰 to explain:

function test(array) {
  console.log(array.length)
  setTimeout(() = > {
    console.log(array.length)
  })
}

const arr = []
test(arr)
arr.push(1)  / / 0 to 1
Copy the code
  • Why print out a 0 and a 1?

    When arr.push(1), the array in test is just a reference to arr. When executed inside setTimeout, fetching the array value is equivalent to fetching the value of arR. So the second print is going to be a 1.

  • Going back to the problem itself, we can see that although the flushInitializers are already empty in the while loop, during promise.all () execution, Maybe we’re pushing content back into READY_INITIALIZERS. So you still need to judge again after the callback.

  • Compare that to the previous example: Initializers are equal to arrays, and READY_INITIALIZERS are equal to ARRs.

    The initializers parameter actually points to READY_INITIALIZERS, so promise.all () still needs to determine whether initializers are empty when the task is finished.

  1. What is READY_INITIALIZERS?

    In the CSR scenario, we mentioned READY_INITIALIZERS:

    if (typeof opts.webpack === "function") {
        READY_INITIALIZERS.push(() = > {
          if (isWebpackReady(opts.webpack)) {
            returninit(); }}); }Copy the code

    If the configuration parameter is passed to WebPack, a task is added to the queue, and when WebpackReady the init function is executed to load the component.

So what does WebpackReady determine?

function isWebpackReady(getModuleIds) {
  if (typeof__webpack_modules__ ! = ="object") {
    return false;
  }

  return getModuleIds().every(moduleId= > {
    return (
      typeofmoduleId ! = ="undefined" &&
      typeof__webpack_modules__[moduleId] ! = ="undefined"
    );
  });
}
Copy the code
  • webpack_modulesIs a variable that comes out of webPack packaging and binds many methods to it. soisWebpackReady First determine if you are in a WebPack environment.
  • getModuleIdsThe return value is the WebPack parameter value passed in React Loadable. It needs to be an array of Moduleids, which will eventually be defined in **webpack_modules** object.
  • Overall,isWebpackReadyThe method is to determine whether the WebPack is loaded and whether the specified lazy load module is defined correctly.

In summary: READY_INITIALIZERS are tasks stored before ReactDOM Render can be loaded.

Why does preloadReady resolve whether it succeeds or fails?

  • First of all, you should know that the essence of preloadReady is whether the dynamic introduction of the lazily loaded module is successful:
function 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;
}
Copy the code
  • LoadableComponent: LoadableComponent: LoadableComponent: LoadableComponent: LoadableComponent: LoadableComponent: LoadableComponent: LoadableComponent: LoadableComponent: LoadableComponent: LoadableComponent: LoadableComponent: LoadableComponent
render() {
      if (this.state.loading || this.state.error) {
        return React.createElement(opts.loading, {
          isLoading: this.state.loading,
          pastDelay: this.state.pastDelay,
          timedOut: this.state.timedOut,
          error: this.state.error,
          retry: this.retry
        });
      } else if (this.state.loaded) {
        return opts.render(this.state.loaded, this.props);
      } else {
        return null; }}Copy the code

The first condition of the render function shows that if a lazy Loading module is introduced and an error is reported, the logic of the Loading component is entered.

The reason why preloadReady succeeds or fails is resolved:

Since Loading failure will lead to rendering of Loading component, it can be understood that this is also a normal situation.

conclusion

React Loadable is a very clever library.

  • Takes advantage of WebPack’s support for dynamic import
  • The Promise, execution result, and loading state corresponding to the asynchronous task are encapsulated in an object, and all information about the asynchronous task can be obtained through this object
  • Support for both CSR and SSR

Welcome to share!