What is react-hot-loader?

The react – hot – loader is a combination of webpack HotModuleReplacementPlugin plug-in implementation react hot update library, can realize the dynamic thermal retention react condition updates.

Module hot update

Before explaining the React-hot-Loader, you must first understand the HotReplacementPlugin(later HRP). Webpack builds a dependency tree based on the entry file. It can be thought of as a DOM tree. For this dependency tree, HRP injects a Module object into each module, representing some information about the module.

The module object

{
    id: string, / / path
    loaded: boolean,
    exports: boolean,
    webpackPolyfill: number,
    exports: export,__proto__: {
        parents: string[], // Parent reference
        children: string[], / / rely onExports of exports I exports of exportsl: not clear,}}Copy the code

Module. hot is the most important part, we can call hot method, we can customize some hot update implementation, such as blocking hot update, or clean some global variables in hot update, you can learn more about module hot replacement, the following two methods we use:

Module.hot method (part only)

I’ll cover only the accept and addStatusHandler methods here

  • accept: (dependencies: string | string[], cb: () => void) => void

The dependency tree is similar to the DOM tree. Each hot update, the updated module will send its updated message up like bubbling. When there is an ACCEPT processing method, the bubbling will stop and the ACCEPT processing will be performed. If I reload it, all the states will disappear

module.hot.accept('./App.js', () => {
  ...dosomething    
})
Copy the code
  • addStatusHandler: (status => void) => void

Register a function to listen for status changes and control our hot updates based on status

module.hot.addStatusHandler(status= > {
  // Respond to the current status......
  // Idle: the process is waiting to call check (see below)
  // check: The process is checking for updates
  // prepare: The process is preparing for an update (for example, downloading the updated module)
  // Ready: This update is ready and available
  // Dispose: The process is calling its dispose function on the module to be replaced
  // apply: The process is calling the ACCEPT handler and reexecuting the self-accepted module
  // ABORT: The update is aborted, but the system remains in the previous state
  // fail: The update has thrown an exception, and the system state has been corrupted
})
Copy the code

Principle and Implementation

  • root.js

In root.js, we get the parent module that references the hot module, that is, the React component that deals with hot updates, and wrap it with HOC

var hot = require("./hot").hot;
let parent;
if (module.hot) {
  // Get all module caches required, similar to nodeJS
  const cache = require.cache;
  if (!module.parents || module.parents.length === 0) {
    throw new Error("no parents!!");
  }
  // Get our component module
  parent = cache[module.parents[0]].// Remove the root.js module so that it is reloaded with each invocation
  delete cache[module.id];
}
export default hot(parent);
Copy the code
  • hot.js

HOC implementation, wrapping our component with HOC, and saving this instance in componentDidMount so that it is updated without reloading, forceUpdate directly

const requireIndirect =
  typeof__webpack_require__ ! = ="undefined" ? __webpack_require__ : require;
reactHotLoader.patch(React, ReactDOM); // Make some changes to React and ReactDOM.// Update the queue
const runInRenderQueue = createQueue((cb) = > {
  if (ReactDOM.unstable_batchedUpdates) {
    ReactDOM.unstable_batchedUpdates(cb);
  } else{ cb(); }});// Hot update processing
const makeHotExport = (sourceModule, moduleId) = > {
  const updateInstances = (a)= > {
    // Get the instance object of the module
    const module = hotModule(moduleId);
    const deepUpdate = (a)= > {
      // forceUpdate for each instance
      runInRenderQueue((a)= > {
        module.instances.forEach((inst) = > inst.forceUpdate());
      });
    };
    deepUpdate();
  };
  if (sourceModule.hot) {
    // Incorrect parameter passed, but can block hot update bubbling (webpack only)
    sourceModule.hot.accept(updateInstances);
    if (sourceModule.hot.addStatusHandler) {
      if (sourceModule.hot.status() === "idle") {
        sourceModule.hot.addStatusHandler((status) = > {
          if (status === "apply") {
            // Start updating the instance when hot updates are receivedupdateInstances(); }}); }}}};/ / generated HOC
export const hot = (sourceModule) = > {
  const moduleId = sourceModule.id || sourceModule.i;
  // Save the instance
  const module = hotModule(moduleId);
  let firstHotRegistered = false;
  makeHotExport(sourceModule, moduleId);
  return (WrappedComponent) = > {
    const Hoc = createHoc(
      WrappedComponent,
      class Hoc extends React.Component {
        componentDidMount() {
          // Save our react instance
          module.instances.push(this);
        }
        componentWillUnmount() {
          module.instances = module.instances.filter((a) = >a ! = =this);
        }
        render() {
          return <WrappedComponent {. this.props} / >; }}); if (! firstHotRegistered) { firstHotRegistered = true; // Save the module, The following will introduce reactHotLoader. Register (WrappedComponent, WrappedComponent displayName | | WrappedComponent. Name, moduleId); } return Hoc; }; };Copy the code
  • reactHotLoader.js

Through the above processing, there is basically a prototype of hot update, but there are still problems

1. Because of the closure, our instance actually points to the original method and forceUpdate will not apply the new code. Even if we manage to make it point to new code, react Tree diff is not the same Component, react will still render again

The solution here is to save the new code for hot updates, create a Proxy on the first load, forceUpdate each time, and let the Proxy find the latest code and execute. This solves the above two problems

const proxies = new Map(a);const types = new Map(a);const resolveType = (type) = > {
  if (type["PROXY_KEY"]) {
    / / get the proxy
    return proxies.get(type["PROXY_KEY"]);
  }
  return type;
};

const reactHotLoader = {
  register(type, name, id) {
    if(! type["PROXY_KEY"]) {
      const key = `${id}-${name}`;
      // Add a flag to the component
      type["PROXY_KEY"] = key;
      // Keep the latest component code by key
      types.set(key, type);
      if(! proxies.get(key)) {// Create a proxy for this component,
        proxies.set(
          key,
          new Proxy(type, {
            apply: function (target, thisBinding, args) {
              const id = target["PROXY_KEY"];
              // Get the latest code
              const latestTarget = types.get(id);
              returnlatestTarget(... args); }})); }}},// Proxy the React. CreateElement method so that we can find the new module code
  patch(React, ReactDOM) {
    if(! React.createElement.isPatchd) {const origin = React.createElement;
      React.createElement = (type, ... args) = >origin(resolveType(type), ... args); React.createElement.isPatchd =true; }}};export default reactHotLoader;
Copy the code

reference

webpack HotReplacementPlugin

github react-hot-loader

Hot Reloading in React(Dan’s Article)

Hot Reloading in React

The source code