preface

First of all, let me introduce the difference between Live reloading and Hot reloading:

  • Live reloading: After modifying the file, Webpack recompiles and forces the browser to refresh, which is a global (entire application) refreshwindow.location.reload();
  • Hot reloading: After the file is modified, Webpack recompiles the corresponding module, which can remember the state of the application when refreshing, so as to achieve partial refresh.

Introduction to the

Fast Refresh is the React Native (V0.6.1) hot replacement (HMR) solution of React Native. As its core implementation is platform-independent, Fast Refresh can also be used on the Web.

The refresh strategy

  • If you edit a module file that only exports the React component, Fast Refresh will only update the module’s code and re-render your component. You can edit anything in a file, including styles, rendering logic, event handling or effects.
  • If you edit a module that does not export the React component, Fast Refresh will re-run that module and any other module files that import it. For example,Button.jsModal.jsAt the same time introducedTheme.js, the editortheme.jsThe time,Button.jsModal.jsWill be updated.
  • And finally, if youEdit a file that is imported by a module outside the React rendering tree,Fast RefreshIt will fall back to full refresh. You may have a file that renders a React component while exporting anotherNot React componentsThe value introduced. For example, your React component module also exports a constant and imports it in non-React component modules. In this case, consider migrating the query to a separate file and importing it into both files. suchFast RefreshBefore it can be reactivated. Other stories are similar.

Fault-tolerant processing

  • If it occurs during Fast RefreshGrammar mistakes, you can save the file again after fixing the error.RedboxThe warning will disappear. Modules with bad syntax will be blocked so you don’t need to reload your App.
  • And if it does,Runtime error during module initialization(For example, willStyleSheet.createInto theStyle.create), the Fast Refresh session continues after you fix the error.RedboxWarning gone, module updated.
  • And if it does,Runtime errors that occur within the componentAfter you fix the bug,Fast RefreshThe sessionalsoWill continue. In this case, React will remount your application with the updated code.
  • If a runtime error occurs in the componentError BoundariesInside, Fast Refresh Will be re-rendered after you fix the bugNodes within error boundaries.

limit

When you edit files, Fast Refresh will keep the state in the component safe. The state in the component is reset after editing the file:

  • The local state of class components is not kept (only the state of function components and Hooks is kept).
  • In addition to the React component, the module you are editing may have other exports.
  • Sometimes a module exports a higher-order component, for examplecreateNavigationContainer(MyScreen). If the returned component is a class component, the state will be reset.

As function components and Hooks are used more widely, the editing experience of Fast Refresh will get better in the long run.

prompt

  • Fast Refresh keeps function components (and Hooks) state by default.
  • Suppose you are debugging an animation that only happens during mount and you want tomandatoryReset the state so that the component can be remounted. In this scenario, you can add anywhere in the file// @refresh reset. This directive causes Fast Refresh to remount the components defined in this file with each edit.

Hooks

Fast Refresh preserves the state of the component as much as possible while editing the Refresh. UseState and useRef, in particular, can keep their old values as long as you don’t change their arguments or the order in which Hooks are called

Dependent hooks — such as useEffect, useMemo, and useCallback — are always refreshed during Fast Refresh. Their list of dependencies is ignored when Fast Refresh is triggered.

For example 🌰, when you change useMemo(() => x * 2, [x]) to useMemo(() => x * 10, [x]), the factory function will run again even if the Hook dependency x has not changed. If React doesn’t do this, the change won’t be reflected on the screen.

Sometimes this mechanism can lead to unexpected results. For example, even if a useEffect dependency is an empty array, it is re-run during Fast Refresh. However, even without Fast Refresh, it is good practice to write useEffect code that can accommodate occasional reruns. This makes it easier for you to introduce new dependencies to it later.

implementation

To achieve a more granular Hot update capability than HMR (Module level) or React Hot Loader (limited component level), support for reliable updates at the component level or even Hooks level is difficult to achieve by external mechanisms (supplementary runtime, compile transformations) and requires deep compatibility with React:

Fast Refresh is a reimplementation of “hot reloading” with full support from React.

That is, some previously unsolvable problems (like Hooks) can now be solved with React

In terms of implementation, Fast Refresh is also based on HMR, from bottom to top:

  • HMR mechanism: such as WebPack HMR
  • Compile conversion:react-refresh/babel
  • Supplementary runtime:react-refresh/runtime
  • React Supports: React DOM 16.9+, or react-Reconciler 0.21.0+

React Hot Loader provides direct support for the React Hot Loader.

The Proxy Component that replaced the Render part of the Component is no longer needed to preserve Component state. React now has native support for hot replacements for functional components and Hooks.

React Refresh is divided into two parts: Babel and Runtime. Both are maintained in the react-refresh package, which is exposed through different entry files (react-refresh/ Babel and react-refresh/ Runtime).

You can understand the implementation of Fast Refresh from the following four aspects:

  1. What does the Babel Plugin do at compile time?
  2. How does the Runtime work together at Runtime?
  3. What support does React provide for this?
  4. Complete mechanism including HMR

What does the Babel Plugin do at compile time?

In simple terms, Fast Refresh finds all components and custom Hooks through the Babel plug-in, and inserts function calls for component registrations and custom Hook signature collections where appropriate.

function useFancyState() {
  const [foo, setFoo] = React.useState(0);
  useFancyEffect();
  return foo;
}

const useFancyEffect = () => {
  React.useEffect(() => {});
};

export default function App() {
  const bar = useFancyState();
  return <h1>{bar}</h1>;
}
Copy the code
var _s = $RefreshSig$(),
    _s2 = $RefreshSig$(),
    _s3 = $RefreshSig$();

function useFancyState() {
  _s();
  const [foo, setFoo] = React.useState(0);
  useFancyEffect();
  return foo;
}

_s(useFancyState, "useState{[foo, setFoo](0)}\nuseFancyEffect{}", false, function () {
  return [useFancyEffect];
});

const useFancyEffect = () => {
  _s2();
  React.useEffect(() => {});
};

_s2(useFancyEffect, "useEffect{}");

export default function App() {
  _s3();
  const bar = useFancyState();
  return <h1>{bar}</h1>;
}

_s3(App, "useFancyState{bar}", false, function () {
  return [useFancyState];
});

_c = App;
var _c;
$RefreshReg$(_c, "App");
Copy the code

How does the Runtime work together at Runtime?

Two undefined functions appear in the code injected by the Babel plug-in:

  • $RefreshSig$Collect user-defined Hook signatures
  • $RefreshReg$Certified components

These two functions come from react-refresh/ Runtime, for example:

var RefreshRuntime = require('react-refresh/runtime');
window.$RefreshReg$ = (type, id) => {
  // Note module.id is webpack-specific, this may vary in other bundlers
  const fullId = module.id + ' ' + id;
  RefreshRuntime.register(type, fullId);
}
window.$RefreshSig$ = RefreshRuntime.collectCustomHooksForSignature;
Copy the code

Corresponding RefreshRuntime createSignatureFunctionForTransform and register

CreateSignatureFunctionForTransform filling Hooks in two stages of identity information, fill in the associated component information for the first time, a second collection Hooks, and a third time after the call is invalid (resolved state, don’t do anything) :

export function createSignatureFunctionForTransform() { let savedType; let hasCustomHooks; let didCollectHooks = false; return function<T>( type: T, key: string, forceReset? : boolean, getCustomHooks? : () => Array<Function>, ): T | void { if (typeof key === 'string') { // We're in the initial phase that associates signatures // with the functions. Note this may be called multiple times // in HOC chains like _s(hoc1(_s(hoc2(_s(actualFunction))))). if (! savedType) { // We're in the innermost call, so this is the actual type. savedType = type; hasCustomHooks = typeof getCustomHooks === 'function'; } // Set the signature for all types (even wrappers!) in case // they have no signatures of their own. This is to prevent // problems like https://github.com/facebook/react/issues/20417. if ( type ! = null && (typeof type === 'function' || typeof type === 'object') ) { setSignature(type, key, forceReset, getCustomHooks); } return type; } else { // We're in the _s() call without arguments, which means // this is the time to collect custom Hook signatures. // Only do this once. This path is hot and runs *inside* every render! if (! didCollectHooks && hasCustomHooks) { didCollectHooks = true; collectCustomHooksForSignature(savedType); }}}; }Copy the code

Register stores component references (type) and component name identifiers (ID) in a large table, and enlists them in the update queue if they already exist:

export function register(type: any, id: string): void {
  // Create family or remember to update it.
  // None of this bookkeeping affects reconciliation
  // until the first performReactRefresh() call above.
  let family = allFamiliesByID.get(id);
  if (family === undefined) {
    family = {current: type};
    allFamiliesByID.set(id, family);
  } else {
    pendingUpdates.push([family, type]);
  }
  allFamiliesByType.set(type, family);
}
Copy the code

The pendingUpdates queue does not take effect until after performReactRefresh is added to the updatedFamiliesByType table.

function resolveFamily(type) { // Only check updated types to keep lookups fast. return updatedFamiliesByType.get(type);  }Copy the code

What support does React provide for this?

Note that Runtime relies on React functions:

import type {
  Family,
  RefreshUpdate,
  ScheduleRefresh,
  ScheduleRoot,
  FindHostInstancesForRefresh,
  SetRefreshHandler,
} from 'react-reconciler/src/ReactFiberHotReloading';
Copy the code

The setRefreshHandler is the key to connect Runtime with React:

export const setRefreshHandler = (handler: RefreshHandler | null): void => { if (__DEV__) { resolveFamily = handler; }};Copy the code

Pass from Runtime to React when performReactRefresh and trigger a React update via ScheduleRoot or scheduleRefresh:

export function performReactRefresh(): RefreshUpdate | null { const update: RefreshUpdate = { updatedFamilies, // Families that will re-render preserving state staleFamilies, // Families that will be remounted }; HelpersByRendererID. ForEach (helpers = > {/ / will update table exposed to React helpers. SetRefreshHandler (resolveFamily); }); / / and trigger the React to update failedRootsSnapshot. ForEach (root = > {const helpers = helpersByRootSnapshot. Get (root); const element = rootElements.get(root); helpers.scheduleRoot(root, element); }); mountedRootsSnapshot.forEach(root => { const helpers = helpersByRootSnapshot.get(root); helpers.scheduleRefresh(root, update); }); }Copy the code

React then fetches the latest functional components and Hooks from resolveFamily:

export function resolveFunctionForHotReloading(type: any): any {
  const family = resolveFamily(type);
  if (family === undefined) {
    return type;
  }
  // Use the latest known implementation.
  return family.current;
}
Copy the code

(from the react/packages/react – the reconciler/SRC/ReactFiberHotReloading. New. Js)

And complete the update during the scheduling process:

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { switch (workInProgress.tag) { case IndeterminateComponent: case FunctionComponent: case SimpleMemoComponent: / / update the functional components workInProgress. Type = resolveFunctionForHotReloading (current) type); break; case ClassComponent: workInProgress.type = resolveClassForHotReloading(current.type); break; case ForwardRef: workInProgress.type = resolveForwardRefForHotReloading(current.type); break; default: break; }}Copy the code

(from the react/packages/react – the reconciler/SRC/ReactFiber. New. Js)

At this point, the whole hot update process is clear

But for the whole thing to work, there’s still one more piece — HMR

including HMR Complete mechanics within

The above is just the ability to have fine-grained hot updates at run time, and to be fully operational and connected to HMR, this part of the work depends on the specific build tools (Webpack, etc.)

Details are as follows:

// const runtime = require('react-refresh/runtime'); / / and inject GlobalHook, hook out something from the React, such as scheduleRefresh runtime. InjectIntoGlobalHook (Windows); window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => type => type; $RefreshReg$= (type, id) => {// Note module.id is webpack-specific, this may vary in other bundlers const fullId = module.id + ' ' + id; RefreshRuntime.register(type, fullId); } window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform; try { // !!! / /... ACTUAL MODULE SOURCE CODE... / /!!!!!! } finally { window.$RefreshReg$ = prevRefreshReg; window.$RefreshSig$ = prevRefreshSig; } // 3. HMR API const myExports = module.exports; if (isReactRefreshBoundary(myExports)) { module.hot.accept(); // Depends on your bundler const runtime = require('react-refresh/runtime'); / / debounce to reduce the frequency of updates let enqueueUpdate = debounce (runtime performReactRefresh, 30); enqueueUpdate(); }Copy the code

Wherein, isReactRefreshBoundary is a specific Hot update strategy to control whether to go to Hot Reloading or degrade to Live Reloading. React the strategy of Native see metro/packages/metro/SRC/lib/polyfills/require js /

use

While Fast Refresh was originally used in React Native, its core implementation is platform-independent and is suitable for Web environments as well.

It’s originally shipping for React Native but most of the implementation is platform-independent.

Install the React Native Metro with webPack or other build tools. For example:

  • Parcel: official support
  • Webpack: Community plug-in

Even the React Hot Loader has posted a decommissioning notice recommending using the officially supported Fast Refresh:

React-Hot-Loader is expected to be replaced by React Fast Refresh. Please remove React-Hot-Loader if Fast Refresh is currently supported on your environment.