React Fast Refresh: 次世代のホットリローディングについて解説
James Reed
Infrastructure Engineer · Leapcell

Preface
まず、Live Reloading と Hot Reloading の違いを紹介しましょう。
- Live Reloading: ファイルが変更されると、Webpack はそれを再コンパイルし、強制的にブラウザをリフレッシュします。これにより、グローバルリフレッシュ(アプリケーション全体)が発生し、
window.location.reload()
と同等になります。 - Hot Reloading: ファイルが変更されると、Webpack は対応するモジュールを再コンパイルしますが、リフレッシュはアプリケーションの状態を保持し、部分的なリフレッシュを可能にします。
Introduction
Fast Refresh は、React が導入した公式の Hot Module Replacement (HMR) ソリューションであり、React Native (v0.6.1) 用です。そのコア実装はプラットフォームに依存しないため、Fast Refresh は Web にも適用できます。
Refresh Strategies
- Reactコンポーネントのみをエクスポートするモジュールファイルを編集すると、Fast Refresh はモジュールのコードのみを更新し、コンポーネントを再レンダリングします。スタイル、レンダリングロジック、イベントハンドラ、エフェクトなど、ファイル内のあらゆるものを編集できます。
- モジュールがReactコンポーネントをエクスポートしない場合、Fast Refresh はモジュールと、それをインポートするすべてのモジュールを再実行します。たとえば、
Button.js
とModal.js
の両方がTheme.js
をインポートする場合、Theme.js
を編集すると、Button.js
とModal.js
の両方が更新されます。 - 最後に、編集するファイルがReactレンダリングツリー外のモジュールによってのみインポートされる場合、Fast Refresh は完全なリロードにフォールバックします。これは、ファイルがReactコンポーネントをレンダリングすると同時に、React以外のモジュールで使用される値をエクスポートする場合に発生する可能性があります。たとえば、Reactコンポーネントモジュールが定数もエクスポートし、React以外のモジュールがそれをインポートする場合、Fast Refresh は単独でファイルを更新できません。このような場合は、エクスポートされた値を別のファイルに移動し、両方のモジュールにインポートすることを検討してください。これにより、Fast Refresh が正しく機能するようになります。
Error Handling
- Fast Refresh 中に構文エラーが発生した場合、エラーを修正してファイルを再度保存できます。赤いエラー画面は消えます。障害のあるモジュールは実行がブロックされているため、アプリをリロードする必要はありません。
- モジュールの初期化中にランタイムエラーが発生した場合(たとえば、誤って
StyleSheet.create
の代わりにStyle.create
と記述した場合)、エラーを修正すると、Fast Refresh セッションを継続できます。エラー画面が消え、モジュールが更新されます。 - コンポーネント内でランタイムエラーが発生した場合、エラーを修正すると、Fast Refresh セッションも再開されます。この場合、React は更新されたコードを使用してコンポーネントを再マウントします。
- クラッシュしたコンポーネントがエラー境界内にある場合、エラーを修正すると、Fast Refresh はエラー境界内のすべてのノードを再レンダリングします。
Limitations
ファイルを編集する際、Fast Refresh は安全な場合にコンポーネントの状態を保持します。ただし、次の場合、ファイル編集後に状態がリセットされます。
- クラスコンポーネントのローカル状態は保持されません(関数型コンポーネントと Hooks の状態のみが保持されます)。
- 編集するモジュールがReactコンポーネント以外のものをエクスポートする場合。
- モジュールが高階コンポーネント(HOC)をエクスポートすることがあります。たとえば、
createNavigationContainer(MyScreen)
のような場合です。返されたコンポーネントがクラスコンポーネントの場合、状態がリセットされます。
関数型コンポーネントと Hooks がより一般的になるにつれて、Fast Refresh を使用した編集エクスペリエンスは向上し続けます。
Tips
- デフォルトでは、Fast Refresh は関数型コンポーネントと Hooks の状態を保持します。
- マウント時にのみ実行されるアニメーションをデバッグしている場合は、編集するたびに完全な再マウントを強制したい場合があります。これを行うには、ファイル内の任意の場所に
// @refresh reset
を追加します。このディレクティブは、編集するたびにそのファイルで定義されたコンポーネントを Fast Refresh に強制的に再マウントさせます。
Hooks Behavior in Fast Refresh
Fast Refresh は編集中にコンポーネントの状態を保持しようとします。具体的には、useState
と useRef
は以前の値を保持します。ただし、次の条件が満たされている場合に限ります。
- パラメーターを変更しない。
- Hook 呼び出しの順序を変更しない。
useEffect
、useMemo
、useCallback
など、依存関係のあるHooks は、Fast Refresh 中に依存関係リストを無視して、常に再実行されます。
たとえば、次のように変更した場合:
useMemo(() => x * 2, [x]);
を
useMemo(() => x * 10, [x]);
x
が変更されていなくても、ファクトリ関数が再度実行されます。この動作がないと、変更が UI に反映されません。
このメカニズムは予期しない動作を引き起こすことがあります。たとえば、useEffect
依存配列が空であっても、Fast Refresh はエフェクトを 1 回再実行します。ただし、一般的に、useEffect
フックは、時折の再実行を処理できるように記述し、後で新しい依存関係を導入しやすくするのが良い習慣です。
Implementation Details
HMR(モジュールレベル)やReact Hot Loader(限定的なコンポーネントレベル)よりもさらにきめ細かい更新を実現するには、コンポーネントレベル、さらにはHooksレベルの信頼性の高い更新をサポートする必要があります。これには、React との深い統合が必要です。ランタイムパッチやコンパイル時変換などの外部メカニズムだけでは不十分です。
Fast Refresh は、React の完全なサポートによる「ホットリロード」の再実装です。
これは、以前は避けられなかった問題(Hooks の処理など)が、React の協力で解決できるようになったことを意味します。
そのコアにおいて、Fast Refresh は引き続き HMR に依存しており、次のレイヤーがあります。
- HMRメカニズム(例:Webpack HMR)
- コンパイル時変換 (
react-refresh/babel
) - ランタイム拡張機能 (
react-refresh/runtime
) - React の組み込みサポート (
React DOM 16.9+
またはreact-reconciler 0.21.0+
)
プロキシコンポーネントを使用するReact Hot Loaderとは異なり、Fast Refresh はこのレイヤーを削除します。React は関数型コンポーネントと Hooks のホットリプレイスメントをネイティブにサポートするようになったためです。
Fast Refresh は、react-refresh
パッケージ内で維持されている2 つの部分で構成されています。
- Babel Plugin (
react-refresh/babel
) - Runtime (
react-refresh/runtime
)
これらの機能は、異なるエントリポイントを介して公開されます。
Fast Refresh の実装を 4 つの重要な側面に分解できます。
- Babel プラグインはコンパイル時に何をするか?
- ランタイムは実行時にどのように機能するか?
- React はこれに対してどのような特定のサポートを提供するか?
- このメカニズムは HMR とどのように統合されるか?
1. What Does the Babel Plugin Do at Compile-Time?
高度なレベルでは、Fast Refresh の Babel プラグインは、コード内のすべてのコンポーネントとカスタム Hooks を検出し、コンポーネントを登録し、Hook シグネチャを収集するための関数呼び出しを挿入します。
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');
_s
関数と_s2
関数はHookシグネチャを収集し、$RefreshReg$
は Fast Refresh 用にコンポーネントを登録します。
2. How Does the Runtime Work at Execution Time?
変換されたコードでは、Babel プラグインによって挿入された2 つの未定義の関数に気付くかもしれません。
$RefreshSig$
: カスタム Hook シグネチャを収集します。$RefreshReg$
: コンポーネントを登録します。
これらの関数は、react-refresh/runtime
から提供されます。一般的な設定は次のようになります。
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;
これらがReact Refresh Runtime API にどのようにマップされるかを以下に示します。
createSignatureFunctionForTransform
: Hook シグネチャ情報を追跡します。register
: 参照 (type
) を 一意の ID (id
) にマッピングして、コンポーネントを登録します。
How createSignatureFunctionForTransform
Works
createSignatureFunctionForTransform
関数は、3 つのフェーズでHook の使用状況を追跡します。
- 初期フェーズ: 関数のシグネチャを対応するコンポーネントに関連付けます。
- Hook 収集フェーズ: コンポーネントで使用されているカスタム Hook に関する情報を収集します。
- 解決済みフェーズ: 3 回目の呼び出し後、不要なオーバーヘッドを回避するために、それ以上の変更の記録を停止します。
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
register
関数はコンポーネントの更新を追跡します。
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); }
内容は次のとおりです。
- コンポーネントがまだ登録されていない場合、グローバルコンポーネントレジストリ(
allFamiliesByID
)に追加されます。 - コンポーネントが既に存在する場合、保留中の更新キュー(
pendingUpdates
)に追加されます。 - 保留中の更新は、Fast Refresh が実行されると後で処理されます。
更新が適用されると、performReactRefresh
は 保留中の更新を**アクティブな更新テーブル(updatedFamiliesByType
)**に移動し、React が関数とコンポーネントの最新バージョンをルックアップできるようにします。
function resolveFamily(type) { return updatedFamiliesByType.get(type); }
3. What Support Does React Provide for Fast Refresh?
React ランタイムは、Fast Refresh 統合のためのいくつかの関数を提供します。
import type { Family, RefreshUpdate, ScheduleRefresh, ScheduleRoot, FindHostInstancesForRefresh, SetRefreshHandler, } from 'react-reconciler/src/ReactFiberHotReloading';
キー関数の 1 つは setRefreshHandler
で、Fast Refresh を React の調整プロセスにリンクします。
export const setRefreshHandler = (handler: RefreshHandler | null): void => { if (__DEV__) { resolveFamily = handler; } };
How Fast Refresh Triggers React Updates
Fast Refresh が更新を検出すると、次の処理を行います。
- 更新テーブル(
updatedFamilies
)を React に渡します。 scheduleRefresh
とscheduleRoot
を使用してReact の更新をトリガーします。
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 は resolveFamily
を使用して、コンポーネントまたは Hooks の最新バージョンを取得します。
export function resolveFunctionForHotReloading(type: any): any { const family = resolveFamily(type); if (family === undefined) { return type; } return family.current; }
レンダリング中に、React は古いコンポーネント参照を新しい参照に置き換えます。
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
これまでのすべてにより、コンポーネントレベルの更新が可能になりますが、Fast Refresh が機能するには、HMR(Hot Module Replacement)との統合が依然として必要です。
HMR Workflow
-
React をロードする前に、ランタイムをアプリケーションに挿入します。
const runtime = require('react-refresh/runtime'); runtime.injectIntoGlobalHook(window); window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => (type) => type;
-
各モジュールを Fast Refresh 登録ロジックでラップします。
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; }
-
すべてのモジュールを処理した後、HMR API にフックします。
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
は、モジュールがホットリロードをサポートしているか、完全なライブリロードが必要かどうかを判断します。
Usage in Web Environments
Fast Refresh は元々 React Native 用に構築されましたが、そのコア実装はプラットフォームに依存しないため、Web アプリケーションでも使用できます。
It’s originally shipping for React Native, but most of the implementation is platform-independent.
Web アプリケーションで Fast Refresh を使用するには、Metro(React Native のバンドラー)を Webpack に置き換え、上記の統合手順に従います。
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