DEV Community

Cover image for Beyond HMR: Understanding React's Fast Refresh
Leapcell
Leapcell

Posted on

Beyond HMR: Understanding React's Fast Refresh

Cover

Preface

First, let's introduce the difference between Live Reloading and Hot Reloading:

  • Live Reloading: When a file is modified, Webpack recompiles it and forcibly refreshes the browser. This results in a global refresh (the entire application), equivalent to window.location.reload().
  • Hot Reloading: When a file is modified, Webpack recompiles the corresponding module, but the refresh retains the application's state, allowing for partial refresh.

Introduction

Fast Refresh is an official Hot Module Replacement (HMR) solution introduced by React for React Native (v0.6.1). Since its core implementation is platform-independent, Fast Refresh is also applicable to the Web.

Refresh Strategies

  • If you edit a module file that only exports React components, Fast Refresh will only update the module's code and re-render your components. You can edit anything within the file, including styles, rendering logic, event handlers, or effects.
  • If the module does not export React components, Fast Refresh will rerun the module and all modules that import it. For example, if both Button.js and Modal.js import Theme.js, editing Theme.js will update both Button.js and Modal.js.
  • Finally, if the file you edit is only imported by a module outside of the React render tree, Fast Refresh falls back to a full reload. This can happen if a file both renders a React component and exports a value used by non-React modules. For example, if a React component module also exports a constant, and a non-React module imports it, Fast Refresh cannot update the file in isolation. In such cases, consider moving the exported value to a separate file and importing it into both modules. This allows Fast Refresh to function correctly.

Error Handling

  • If there is a syntax error during Fast Refresh, you can fix the error and save the file again. The red error screen will disappear. The faulty module is blocked from execution, so you don't need to reload the app.
  • If a runtime error occurs during module initialization (e.g., mistakenly writing Style.create instead of StyleSheet.create), fixing the error allows the Fast Refresh session to continue. The error screen disappears, and the module updates.
  • If a runtime error occurs inside a component, fixing the error also resumes the Fast Refresh session. In this case, React will remount the component using the updated code.
  • If the component that crashes is inside an error boundary, Fast Refresh will re-render all nodes within the error boundary once you fix the error.

Limitations

When editing files, Fast Refresh preserves component state when safe. However, in the following cases, state resets after a file edit:

  • Class components' local state is not retained (only functional components and Hooks' state are preserved).
  • If the module you edit exports anything other than React components.
  • Sometimes, a module exports a higher-order component (HOC), such as createNavigationContainer(MyScreen). If the returned component is a class component, state resets.

As functional components and Hooks become more common, the editing experience with Fast Refresh will continue to improve.

Tips

  • By default, Fast Refresh preserves state in functional components and Hooks.
  • If you are debugging an animation that only runs on mount, you may want to force a full remount on every edit. To do this, add // @refresh reset anywhere in the file. This directive forces Fast Refresh to remount the components defined in that file each time you edit it.

Hooks Behavior in Fast Refresh

Fast Refresh tries to preserve component state during edits. Specifically, useState and useRef retain their previous values, as long as:

  • You do not change their parameters.
  • You do not alter the order of Hook calls.

Hooks with dependencies—such as useEffect, useMemo, and useCallbackalways re-run during Fast Refresh, ignoring dependency lists.

For example, if you change:

useMemo(() => x * 2, [x]);
Enter fullscreen mode Exit fullscreen mode

to

useMemo(() => x * 10, [x]);
Enter fullscreen mode Exit fullscreen mode

Even if x remains unchanged, the factory function runs again. Without this behavior, changes would not be reflected in the UI.

This mechanism sometimes produces unexpected behavior. For example, even if a useEffect dependency array is empty, Fast Refresh still reruns the effect once. However, it is generally a good practice to write useEffect hooks that can handle occasional re-execution, making it easier to introduce new dependencies later.

Implementation Details

To achieve finer-grained updates than HMR (module-level) and React Hot Loader (limited component-level), we need to support component-level and even Hooks-level reliable updates. This requires deep integration with React, as external mechanisms alone (such as runtime patches or compile-time transformations) are insufficient:

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

This means that previously unavoidable issues (e.g., handling Hooks) can now be solved with React's cooperation.

At its core, Fast Refresh still relies on HMR, with the following layers:

  1. HMR Mechanism (e.g., Webpack HMR)
  2. Compile-time Transformation (react-refresh/babel)
  3. Runtime Enhancements (react-refresh/runtime)
  4. React's Built-in Support (React DOM 16.9+ or react-reconciler 0.21.0+)

Unlike React Hot Loader, which uses proxy components, Fast Refresh removes this layer, as React now natively supports hot replacement for functional components and Hooks.

Process

Fast Refresh consists of two parts, both maintained within the react-refresh package:

  • Babel Plugin (react-refresh/babel)
  • Runtime (react-refresh/runtime)

It exposes these features via different entry points.

We can break down Fast Refresh’s implementation into four key aspects:

  1. What does the Babel plugin do at compile-time?
  2. How does the runtime work at execution time?
  3. What specific support does React provide for this?
  4. How does this mechanism integrate with HMR?

1. What Does the Babel Plugin Do at Compile-Time?

At a high level, Fast Refresh’s Babel plugin detects all components and custom Hooks in the code and inserts function calls to register components and collect Hook signatures.

Before Transformation

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>;
}
Enter fullscreen mode Exit fullscreen mode

After Transformation

var _s = $RefreshSig$(),
  _s2 = $RefreshSig$(),
  _s3 = $RefreshSig$();

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

_s(useFancyState, 'useState{ct{}', 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');
Enter fullscreen mode Exit fullscreen mode

The _s and _s2 functions collect Hook signatures, while $RefreshReg$ registers components for Fast Refresh.

2. How Does the Runtime Work at Execution Time?

In the transformed code, you may notice two undefined functions injected by the Babel plugin:

  • $RefreshSig$: Collects custom Hook signatures.
  • $RefreshReg$: Registers components.

These functions come from react-refresh/runtime. A typical setup might look like this:

var RefreshRuntime = require('react-refresh/runtime');

window.$RefreshReg$ = (type, id) => {
  // Note: `module.id` is Webpack-specific; other bundlers may use different identifiers.
  const fullId = module.id + ' ' + id;
  RefreshRuntime.register(type, fullId);
};

window.$RefreshSig$ = RefreshRuntime.collectCustomHooksForSignature;
Enter fullscreen mode Exit fullscreen mode

Here’s how they map to React Refresh Runtime API:

  • createSignatureFunctionForTransform: Tracks Hook signature information.
  • register: Registers components by mapping their references (type) to unique IDs (id).

How createSignatureFunctionForTransform Works

The createSignatureFunctionForTransform function tracks Hook usage in three phases:

  1. Initial Phase: Associates a function signature with its corresponding component.
  2. Hook Collection Phase: Gathers information about custom Hooks used in the component.
  3. Resolved Phase: After the third call, it stops recording further changes (to prevent unnecessary overhead).
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') {
      if (!savedType) {
        savedType = type;
        hasCustomHooks = typeof getCustomHooks === 'function';
      }
      if (type != null && (typeof type === 'function' || typeof type === 'object')) {
        setSignature(type, key, forceReset, getCustomHooks);
      }
      return type;
    } else {
      if (!didCollectHooks && hasCustomHooks) {
        didCollectHooks = true;
        collectCustomHooksForSignature(savedType);
      }
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

How register Works

The register function tracks component updates:

export function register(type: any, id: string): void {
  let family = allFamiliesByID.get(id);
  if (family === undefined) {
    family = { current: type };
    allFamiliesByID.set(id, family);
  } else {
    pendingUpdates.push([family, type]);
  }
  allFamiliesByType.set(type, family);
}
Enter fullscreen mode Exit fullscreen mode

Here’s what happens:

  1. If the component is not yet registered, it is added to a global component registry (allFamiliesByID).
  2. If the component already exists, it is added to a pending update queue (pendingUpdates).
  3. The pending updates are processed later when Fast Refresh runs.

When updates are applied, performReactRefresh moves pending updates to an active update table (updatedFamiliesByType), allowing React to look up the latest versions of functions and components:

function resolveFamily(type) {
  return updatedFamiliesByType.get(type);
}
Enter fullscreen mode Exit fullscreen mode

3. What Support Does React Provide for Fast Refresh?

The React runtime provides several functions for Fast Refresh integration:

import type {
  Family,
  RefreshUpdate,
  ScheduleRefresh,
  ScheduleRoot,
  FindHostInstancesForRefresh,
  SetRefreshHandler,
} from 'react-reconciler/src/ReactFiberHotReloading';
Enter fullscreen mode Exit fullscreen mode

One key function is setRefreshHandler, which links Fast Refresh to React’s reconciliation process:

export const setRefreshHandler = (handler: RefreshHandler | null): void => {
  if (__DEV__) {
    resolveFamily = handler;
  }
};
Enter fullscreen mode Exit fullscreen mode

How Fast Refresh Triggers React Updates

When Fast Refresh detects an update, it does the following:

  1. Passes the update table (updatedFamilies) to React.
  2. Triggers React updates using scheduleRefresh and scheduleRoot:
export function performReactRefresh(): RefreshUpdate | null {
  const update: RefreshUpdate = {
    updatedFamilies, // Components that will re-render while preserving state
    staleFamilies, // Components that must be remounted
  };

  helpersByRendererID.forEach((helpers) => {
    helpers.setRefreshHandler(resolveFamily);
  });

  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);
  });
}
Enter fullscreen mode Exit fullscreen mode

How React Uses the Updated Components

React uses resolveFamily to get the newest version of components or Hooks:

export function resolveFunctionForHotReloading(type: any): any {
  const family = resolveFamily(type);
  if (family === undefined) {
    return type;
  }
  return family.current;
}
Enter fullscreen mode Exit fullscreen mode

During rendering, React swaps out the old component reference for the new one:

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case FunctionComponent:
    case SimpleMemoComponent:
      workInProgress.type = resolveFunctionForHotReloading(current.type);
      break;
    case ClassComponent:
      workInProgress.type = resolveClassForHotReloading(current.type);
      break;
    case ForwardRef:
      workInProgress.type = resolveForwardRefForHotReloading(current.type);
      break;
    default:
      break;
  }
}
Enter fullscreen mode Exit fullscreen mode

4. How Fast Refresh Integrates with HMR

Everything so far enables component-level updates, but for Fast Refresh to work, it still needs integration with HMR (Hot Module Replacement).

HMR Workflow

  1. Inject the runtime into the application before loading React:
   const runtime = require('react-refresh/runtime');
   runtime.injectIntoGlobalHook(window);
   window.$RefreshReg$ = () => {};
   window.$RefreshSig$ = () => (type) => type;
Enter fullscreen mode Exit fullscreen mode
  1. Wrap each module with Fast Refresh registration logic:
   window.$RefreshReg$ = (type, id) => {
     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;
   }
Enter fullscreen mode Exit fullscreen mode
  1. After processing all modules, hook into HMR APIs:
   const myExports = module.exports;

   if (isReactRefreshBoundary(myExports)) {
     module.hot.accept(); // Depends on the bundler
     const runtime = require('react-refresh/runtime');

     let enqueueUpdate = debounce(runtime.performReactRefresh, 30);
     enqueueUpdate();
   }
Enter fullscreen mode Exit fullscreen mode

isReactRefreshBoundary determines whether a module supports Hot Reloading or needs a full Live Reload.

Usage in Web Environments

Although Fast Refresh was originally built for React Native, its core implementation is platform-independent, making it usable for web applications as well.

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

To use Fast Refresh in web applications, replace Metro (React Native’s bundler) with Webpack and follow the integration steps outlined above.


We are Leapcell, your top choice for hosting Node.js projects.

Leapcell

Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:

Multi-Language Support

  • Develop with Node.js, Python, Go, or Rust.

Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the Documentation!

Try Leapcell

Follow us on X: @LeapcellHQ


Read on our blog

Top comments (0)