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
andModal.js
importTheme.js
, editingTheme.js
will update bothButton.js
andModal.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 ofStyleSheet.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 useCallback
—always re-run during Fast Refresh, ignoring dependency lists.
For example, if you change:
useMemo(() => x * 2, [x]);
to
useMemo(() => x * 10, [x]);
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:
- HMR Mechanism (e.g., Webpack HMR)
-
Compile-time Transformation (
react-refresh/babel
) -
Runtime Enhancements (
react-refresh/runtime
) -
React's Built-in Support (
React DOM 16.9+
orreact-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.
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:
- What does the Babel plugin do at compile-time?
- How does the runtime work at execution time?
- What specific support does React provide for this?
- 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>;
}
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');
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;
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:
- Initial Phase: Associates a function signature with its corresponding component.
- Hook Collection Phase: Gathers information about custom Hooks used in the component.
- 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);
}
}
};
}
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);
}
Here’s what happens:
- If the component is not yet registered, it is added to a global component registry (
allFamiliesByID
). - If the component already exists, it is added to a pending update queue (
pendingUpdates
). - 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);
}
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';
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;
}
};
How Fast Refresh Triggers React Updates
When Fast Refresh detects an update, it does the following:
-
Passes the update table (
updatedFamilies
) to React. -
Triggers React updates using
scheduleRefresh
andscheduleRoot
:
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);
});
}
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;
}
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;
}
}
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
- Inject the runtime into the application before loading React:
const runtime = require('react-refresh/runtime');
runtime.injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;
- 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;
}
- 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();
}
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 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!
Follow us on X: @LeapcellHQ
Top comments (0)