Are you familiar with react-refresh?
Yes, I'm familiar with
react-refresh
. It's the underlying technology that powers
Fast Refresh
in React development environments—most notably with
tools like
Vite,
Webpack, and
Next.js
(via plugins or built-in support).
Here’s a quick breakdown:
react-refresh
?
react-refresh
is a
hot module replacement (HMR)
system specifically designed for React. It lets you
preserve the state of your components
while updating their code during development—so when
you make changes to your components, the UI updates
instantly
without a full reload
and
without losing component state.
It hooks into your bundler (like Webpack or Vite) and watches for code changes.
When a change is detected, it swaps out just the changed modules.
If the change is non-breaking (e.g., a small tweak in render logic), it preserves the component’s state.
If not (e.g., component structure changes), it falls back to reloading the affected components.
You don't have to do anything—vite
handles it with
@vitejs/plugin-react
.
You’d use:
bashnpm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
And add it in your
webpack.config.js
:
jsconst ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); module.exports = { // ... plugins: [ isDev && new ReactRefreshWebpackPlugin(), // other plugins ].filter(Boolean), module: { rules: [ { test: /\.(js|jsx|ts|tsx)$/, loader: require.resolve('babel-loader'), options: { plugins: [isDev && require.resolve('react-refresh/babel')].filter(Boolean), }, }, ], }, };
Let me know if you're troubleshooting something specific or want a setup example for a certain framework!
Great question! The logic behind
react-refresh
is clever and elegant. It’s designed to
preserve React component state
across code edits
during development, without needing a full reload. Let's break down
how it works under the hood:
react-refresh
Runtime Tracking of Components
React components are
registered
with the
react-refresh
runtime.
This happens via the
react-refresh/babel
plugin, which injects code to register each
component function with a unique ID.
This ID is typically derived from the file name + export name, so React can track the identity of each component.
Hot Swapping Logic
When you edit a component’s code, the module gets recompiled and re-executed.
react-refresh
compares the new component
type
with the previous one (i.e., the function or
class definition).
If the change is considered safe (e.g., you updated JSX or logic inside the function), it replaces the old version with the new one.
If the component structure changed significantly (e.g., class to function or changes to hooks structure), it discards the state and re-renders.
State Preservation via Element Types
React’s reconciliation logic sees that the “type” of the component is the same (thanks to the registration mechanism).
So it can reuse the existing instance and its state.
Think of it like this: instead of replacing the whole component tree, it surgically swaps out just the internals of the affected components.
Error Boundaries for Stability
If something breaks during the refresh (e.g.,
a syntax error or hook misusage),
react-refresh
can fall back gracefully.
Most dev setups wrap the app in an error overlay (like the red box in Next.js or Vite) so you can fix the issue without killing the whole app state.
There’s a concept of refresh boundaries:
A file (or module) is considered a Refresh Boundary if all exports are React components or things that support hot-swapping.
If a boundary changes in a way that might break the state, the runtime discards the old state and does a remount.
So:
✅ Editing JSX → state preserved
❌ Changing number/order of hooks → state reset
❌ Changing exports in a non-component module → full reload
Since React is declarative and virtual-DOM based, it’s especially suited to this kind of fine-grained hot replacement. The key is maintaining the identity of components between edits so React doesn’t think it needs to tear everything down.
If you’re interested, I can show a mini working example with code or even a visual explanation of the lifecycle!
Absolutely! Let's break it into two parts:
+------------------------+ | MyComponent | |------------------------| | useState: count = 0 | | JSX: <button>Click</>| +------------------------+ User clicks → count = 1
js// Change: Text on the button <button>Click me!</button>
Babel plugin injects registration code.
Hot module replacement runs the new module.
react-refresh
runtime checks:
Is
MyComponent
still a valid boundary?
Is the shape of the component safe?
If yes → swap in new function, keep old state:
+------------------------+ | MyComponent (updated) | |------------------------| | useState: count = 1 | <-- STATE PRESERVED | JSX: <button>Click me!</> | +------------------------+
User sees updated text — no reload, state intact.
js// Change: useEffect → useState useState(...) → useEffect(...)
react-refresh
detects change in
hook structure
→ Not safe.
🔥 It invalidates the module and remounts:
Component reloaded → count resets to 0
Let’s say you have a React component like this:
jsx// Counter.jsx import React, { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> Count: {count} </button> ); }
Now in
webpack.config.js
, you configure
react-refresh
:
jsconst ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); module.exports = { mode: 'development', entry: './src/index.js', module: { rules: [ { test: /\.(js|jsx)$/, loader: 'babel-loader', options: { plugins: [require.resolve('react-refresh/babel')], }, }, ], }, plugins: [new ReactRefreshWebpackPlugin()], devServer: { hot: true, }, };
json{ "presets": ["@babel/preset-react"], "plugins": ["react-refresh/babel"] }
Now when you edit
Counter.jsx
(e.g., change the text),
React Refresh updates only that component, keeps the state (like the current count), and the
UI reflects the change instantly.
Want me to generate a full working repo template you can run locally or play with in StackBlitz/CodeSandbox?
Great follow-up! The magic of
react-refresh
detecting changes lies in
module-level registration, runtime tracking, and
Babel transformations. Let’s break it down step-by-step:
react-refresh
Detects Component Changes
The first step is the Babel plugin:
react-refresh/babel
.
It wraps each React component in a registration call.
This tells the runtime:
🧠 “Hey, this file exports a component named
MyComponent
— track it.”
Example:
js// Original code export function MyComponent() { ... } // Transformed code (simplified): const MyComponent = () => { ... }; $RefreshReg$(MyComponent, "MyComponent"); export { MyComponent };
So now every component has:
A name
A module ID
A registration in the refresh runtime
react-refresh
maintains an internal map of modules and their
exports:
Which modules export React components?
What are the “signatures” (structure) of those components?
It stores this info so it can later compare before and after versions.
Your dev server (like Webpack or Vite) notices a file change and triggers HMR.
react-refresh
receives the new version of the module.
Now it performs a diff:
Has the shape of the module exports changed?
Do the component signatures match?
If the export list is the same, and the component’s
internal logic looks compatible (e.g., same
number/order of hooks),
react-refresh
:
Swaps the old component function with the new one
Preserves local state via React’s reconciliation
If the runtime detects:
A new hook added/removed/changed order
A changed export list (e.g., removed/renamed component)
A switch from class to function, or vice versa
Then:
It invalidates the component and forces a remount
In some cases, it triggers a full reload of the module
This avoids weird bugs or broken component trees.
react-refresh
uses
hook signatures
to verify safe updates.
This is enabled by the
react-refresh/babel
plugin.
It injects metadata about the hook order and type.
When you change a hook (e.g., add/remove one), it detects a mismatch in the hook signature and flags it as unsafe.
Step | Action |
---|---|
1️⃣ Babel plugin | Tags each component with metadata |
2️⃣ Runtime | Tracks module exports & component identities |
3️⃣ File change | Module hot-reloaded |
4️⃣ Component diff | Checks if update is “safe” |
✅ If safe | Replaces code, keeps state |
❌ If unsafe | Reloads component or module |
Let me know if you'd like a visual diff demo, or a minimal custom implementation that shows this detection logic in action!
This is where things get
really smart —
the key is
how React reconciles component trees
and how
react-refresh
leverages that to swap component implementations
without touching the instance. Here's the breakdown:
react-refresh
Refreshes a Component While Preserving State
React’s Virtual DOM already knows how to:
Compare component
types
(function Foo()
vs
function Bar()
)
Re-render only when the type changes
Keep internal state (like
useState
,
useReducer
, etc.) as long as the component
identity stays the same
So if you change the code of a component but keep the same type and props:
jsx<MyComponent key="same" />
React will reuse the existing instance, including:
Its
useState()
hooks
Its
useEffect()
dependencies
Any refs, context, etc.
react-refresh
Adds to the Equation
Here’s what
react-refresh
does
on top of that
to make updates live during development:
js// Before: function MyComponent() { const [count, setCount] = useState(0); return <div>{count}</div>; } // After (code edited): function MyComponent() { const [count, setCount] = useState(0); return <div>Count is: {count}</div>; }
The runtime says:
“Hey React, use this new version of
MyComponent
, but don’t re-render the parent or throw away the existing instance.”
And React says:
“Cool, the type is the same — I’ll keep state.”
react-refresh
creates a
proxy around your component, so the rendered tree always uses the
latest version
of the function.
Think of it like this:
js// This is what React renders: const currentComponent = getCurrentVersion("MyComponent"); return React.createElement(currentComponent, props);
So when the function body changes, it doesn’t remount — it just uses the new logic next render.
React uses an internal data structure called the Fiber tree, where each component has a node storing:
Its type (component function)
Its hooks/state
Its children
As long as the identity of the component fiber remains stable (same key, same parent), React won’t destroy the node.
So:
react-refresh
updates the
type
React re-renders the same fiber with the new implementation
✅ State is preserved
js// React’s internal fiber: fiber = { type: OldComponent, stateNode: { count: 2 }, // this is preserved alternate: ... } // After refresh: fiber.type = NewComponent; // swapped in React schedules an update
If your edit:
Changes the hook order or count
Adds/removes
useState
,
useEffect
, etc.
Changes a function to a class (or vice versa)
Then hook state can't be reused, and React throws a warning or resets the component.
Edit Type | State Preserved? | Why? |
---|---|---|
Change JSX | ✅ | Same function type, same hooks |
Change logic inside
useEffect
|
✅ | Hook identity preserved |
Add a new
useState
|
❌ | Hook order changed |
Rename export | ❌ | Component identity lost |
Change from class to function | ❌ | Component type changed |
Would you like a visual example of how React’s fiber tree handles this under the hood? Or maybe a sandbox to experiment with these changes yourself?
Exactly — that’s the perfect short version. Here's your summary with a bit more structure for clarity:
react-refresh
Does
"It patches in the new version of a component and triggers a re-render, while keeping the original state and props intact — as long as the component’s identity and hook signature remain compatible."
🔁 Swaps component implementation
Replaces the
function body
of the component in memory using a proxy or
runtime reference.
🧠 Keeps the component’s identity
As long as the export name and component type
don’t change, React treats it as “the same”
component.
💾 Preserves internal state and props
Since the component instance (the React
fiber node) isn’t unmounted,
useState
,
useReducer
, and props all stay as-is.
🪛 Forces a re-render
After swapping the implementation, it schedules a
re-render so you see the updated UI right away.
So yes: it's like surgical hot-patching of your component, keeping its memory and lifecycle intact, only changing the logic.
Let me know if you'd like a mental model cheat sheet or a diagram — happy to whip that up!
Great question — you're zooming into the
real core of
react-refresh
. While React itself handles diffing for UI updates
via the Virtual DOM,
react-refresh
does its own
module-level diffing
to decide whether a refresh can happen safely.
Let’s unpack how that diffing works:
react-refresh
Detects Safe vs Unsafe Component Changes
Unlike React’s UI diff (virtual DOM diffing),
react-refresh
performs a
runtime diff of modules and exports, not of rendered output.
It doesn’t “diff” like Git (line by line). Instead, it checks structural and identity-level consistency in two key layers:
Each time you save a file, your bundler (like Webpack
or Vite) reloads the module.
react-refresh
does this:
jsOld module: export const MyComponent = () => { ... }; New module: export const MyComponent = () => { ... };
If:
The number and names of exports are the same,
Each export is a valid React component,
Then the module is marked as a Refresh Boundary — meaning it's eligible for in-place refresh.
Beyond export names,
react-refresh
uses
hook signatures
to decide whether it's safe to preserve state.
Injected by Babel plugin:
jsfunction MyComponent() { useState(...); // Line 1 useEffect(...); // Line 2 }
react-refresh
tracks:
Number of hooks
Order of calls
Type of hook (useState
,
useEffect
, etc.)
If you:
Change hook order
Add/remove a hook
Change a hook’s type
👉 Signature changes → ❌ Unsafe → Refresh resets state
Checkpoint | Purpose |
---|---|
Export count and names | Does this module export the same stuff? |
Export values | Are those exports React components? |
Hook signature (via Babel) | Are the internals of the component safe? |
React type (function ,
class )
|
Did the component’s definition change? |
If all of the above pass → ✅ Safe to swap and
preserve state
If any fail → ❌ Invalidate component and re-render
from scratch
React Refresh runtime exposes methods like:
js$RefreshReg$(Component, "ComponentName"); $RefreshSig$();
These are used to:
Register components
Attach signatures
Compare old and new registrations
On reload, it checks:
js// Can we keep this boundary and just refresh it? isReactRefreshBoundary(currentModule)
In simple terms:
If the shape of your file’s exports and your components’ internal hooks haven’t changed in a dangerous way,
react-refresh
will patch in your new code and let React do a seamless re-render.
If not:
It throws out the component and re-renders it fresh.
Would you like to see what a signature comparison looks like in the Babel output, or maybe a code example where you can intentionally break the refresh to see how the diff reacts?
You're asking the
heart of the
safety mechanism — and it’s such a cool trick! React
doesn’t do this on its own. Instead,
react-refresh/babel
injects runtime code that tracks hook
signatures, which are then used to detect whether the
order, number, or type of hooks
has changed between refreshes.
Let’s break down exactly how it knows about the hook order:
When you use the
react-refresh/babel
plugin, it
rewrites your component code
to include a special function called
__REACT_REFRESH_SIGNATURE__
.
Here’s what happens:
jsfunction MyComponent() { const [count, setCount] = useState(0); useEffect(() => { ... }, []); return <div>{count}</div>; }
jsconst MyComponent = __REACT_REFRESH_SIGNATURE__()(function MyComponent() { const [count, setCount] = useState(0); useEffect(() => { ... }, []); return <div>{count}</div>; });
The
__REACT_REFRESH_SIGNATURE__()
function captures a
hash
representing:
The number of hooks
The order they are called
The
type
of each hook (useState
,
useEffect
, etc.)
At runtime, when a component is registered for refresh, its hook signature is:
Captured and stored
Compared to the previous version's signature (from before your last save)
If the hook signature
does not match,
react-refresh
marks the component as
"not refresh-safe", and it gets
remounted, losing its state.
Let’s say this is the first version:
jsfunction MyComponent() { useState(0); // Hook 1 useEffect(() => {}, []); // Hook 2 }
Hook signature might be:
["useState", "useEffect"]
Now you edit it:
jsfunction MyComponent() { useEffect(() => {}, []); // Hook 1 useState(0); // Hook 2 }
New signature:
["useEffect", "useState"]
Mismatch detected → Not safe to refresh → State is reset
jsfunction MyComponent() { useState(0); useEffect(() => {}, []); useRef(null); // New hook added }
New signature:
["useState", "useEffect", "useRef"]
→ Signature changed → Not safe → Full remount.
Change logic inside hooks
Change JSX
Rename variables
Update props
Add/remove a hook
Change hook order
Change hook type at a position (e.g.,
useState
to
useReducer
)
react-refresh/babel
injects a signature hash that reflects the exact sequence and type of hooks used in your component. At runtime, that signature is compared to the previous version. If it changes, the refresh is considered unsafe, and React remounts the component.
It’s a clever hack layered on top of React’s rules of hooks and designed to prevent weird bugs from hook mismatches.
Would you like to see what the Babel output actually looks like in a real component?
Great — you're now asking the
deep internals,
and it's a fun one. Let’s unpack how
__REACT_REFRESH_SIGNATURE__
works and
how it tracks the hooks, even though hooks are just function calls in your
component.
The
__REACT_REFRESH_SIGNATURE__
doesn't literally track the hooks as they run. Instead, it tags the component with metadata during Babel compilation, which acts like a fingerprint (signature) of the hooks’ order and type — not the values or calls themselves.
It’s static analysis at build time, not runtime execution of hook calls.
When you use
react-refresh/babel
, it analyzes your component and wraps it like this:
jsconst MyComponent = __REACT_REFRESH_SIGNATURE__()(function MyComponent() { const [count, setCount] = useState(0); useEffect(() => {}, []); return <div>{count}</div>; });
But that’s just a wrapper — the real stuff happens behind the scenes:
The Babel plugin generates something like this (simplified):
jsconst $ReactRefreshSig$ = () => { const sig = runtime.createSignatureFunctionForTransform(); sig.__hmr_hook_types__ = ['useState', 'useEffect']; return sig; }; const MyComponent = $ReactRefreshSig$()(function MyComponent() { // your original code });
Now:
createSignatureFunctionForTransform()
returns a "tracker"
It’s annotated with the types and order of hooks as strings
This is done at compile time — Babel analyzes your code and records:
"The first hook is
useState
"
"The second is
useEffect
"
etc.
When you hot-reload the module:
react-refresh
compares the new
__hmr_hook_types__
array to the previous version’s.
If they’re identical → ✅ safe update
If they differ → ❌ remount the component
React has strict Rules of Hooks — hooks must:
Be called unconditionally
Be called in the same order
Be called directly in the component body
That makes static analysis (via Babel) actually feasible and reliable!
Concept | What it does |
---|---|
__REACT_REFRESH_SIGNATURE__
|
A function injected by Babel that wraps your component |
createSignatureFunctionForTransform()
|
Generates an object used to store hook info |
__hmr_hook_types__
|
An array like
["useState", "useEffect"]
used to compare old/new
|
Tracking method | Done via static analysis during Babel transformation |
Runtime comparison | Uses this metadata to decide if the component can be hot-swapped |
Would you like to see a real-world example using Babel output or even step through one using a React + Vite + Babel playground?
Yes — exactly!
react-refresh/babel
analyzes the raw code at build time
(not runtime) to extract information about the
hooks used in your component, based on
how they appear in the code's structure.
It’s a form of static analysis, meaning it reads your component’s source code and determines:
Where hooks are called
In what order
What kind of hooks they are (useState
,
useEffect
, etc.)
All without running the code.
Let’s dig a little deeper:
✅ Yes — using AST (Abstract Syntax Tree) traversal.
Babel parses your source code into a structured tree of nodes, representing:
Variables
Function calls
Import statements
etc.
Example:
jsfunction MyComponent() { const [count, setCount] = useState(0); useEffect(() => {}, []); return <div>{count}</div>; }
Turns into AST nodes like:
jsFunctionDeclaration BlockStatement VariableDeclaration (useState) ExpressionStatement (useEffect) ReturnStatement (JSX)
The
react-refresh/babel
plugin walks the AST to:
Find all functions that look like React components
Find any top-level calls to functions whose names
match
use*
(like
useState
,
useEffect
,
useReducer
, etc.)
Since React's Rules of Hooks say you can only call hooks:
At the top level of your component
In the same order every time
…this static scan is enough to build a "hook fingerprint" of your component.
Once it finds the hooks, it records:
js['useState', 'useEffect']
And assigns that to:
jssig.__hmr_hook_types__ = ['useState', 'useEffect'];
Then wraps the component with:
jsconst MyComponent = $ReactRefreshSig$()(function MyComponent() { ... });
This is all inserted at build time by Babel.
Execute your code
Evaluate the value of variables
Detect conditional hooks (those would break the rules!)
It’s all based on how the hooks appear statically in the code, not what they do or return.
Because React enforces strict rules on how hooks are used, this static approach is:
Lightweight
Fast
Safe in dev mode
Question | Answer |
---|---|
Does it analyze raw code? | ✅ Yes, using Babel's AST system |
What does it extract? | The order and type of hooks used |
Is it runtime or compile time? | 🛠️ Compile-time, during Babel transform |
Does it run your code? | ❌ No, it's static analysis only |
Want to see an actual AST snippet or how you could write a tiny Babel plugin that finds hooks? I can demo that too!
Awesome — let’s build a
minimal Babel plugin
that detects hooks inside React components! This will
help you understand how
react-refresh/babel
works under the hood.
A custom Babel plugin that:
Finds all React function components
Detects hook calls inside them
Logs or stores the list of hook types used
We’ll keep it simple: log the names of hooks like
useState
,
useEffect
, etc.
Create a file like
babel-plugin-track-hooks.js
:
jsmodule.exports = function ({ types: t }) { return { visitor: { FunctionDeclaration(path) { if (isComponent(path)) { const hooks = findHooks(path); console.log(`🔍 ${path.node.id.name}:`, hooks); } }, VariableDeclaration(path) { // Also detect arrow function components const decl = path.node.declarations[0]; if ( decl.init && (t.isArrowFunctionExpression(decl.init) || t.isFunctionExpression(decl.init)) && isComponentFunctionName(decl.id.name) ) { const hooks = findHooks(path.get('declarations.0.init')); console.log(`🔍 ${decl.id.name}:`, hooks); } } } }; function isComponent(path) { const name = path.node.id && path.node.id.name; return isComponentFunctionName(name); } function isComponentFunctionName(name) { return name && /^[A-Z]/.test(name); } function findHooks(fnPath) { const hooks = []; fnPath.traverse({ CallExpression(callPath) { const callee = callPath.get('callee'); if (callee.isIdentifier() && /^use[A-Z0-9]/.test(callee.node.name)) { hooks.push(callee.node.name); } } }); return hooks; } };
Install Babel CLI if you don’t have it:
bashnpm install --save-dev @babel/core @babel/cli
Create a test file, e.g.
App.jsx
:
jsximport React, { useState, useEffect } from 'react'; export default function App() { const [count, setCount] = useState(0); useEffect(() => { console.log('Mounted'); }, []); return <div>{count}</div>; }
Now run Babel with your plugin:
bashnpx babel App.jsx --plugins ./babel-plugin-track-hooks.js
You should see output like:
🔍 App: [ 'useState', 'useEffect' ]
✅ Parsed React components
✅ Found all top-level hook calls
✅ Matched by name pattern
useXyz
✅ Logged the order and types — just like
react-refresh
does internally
Attach
__hmr_hook_types__
like
react-refresh
does
Save and compare signatures across builds
Invalidate or refresh based on changes
Want me to extend this plugin to simulate real signature injection or show how it fits into a dev toolchain like Vite or Webpack?