React Fast Refresh: What Actually Happens When You Save a File
I've been using Fast Refresh in React projects since the early days of Next.js 9.4. The claim is that it preserves component state while you edit code, which sounds almost too good to be true. It works most of the time, but there are edge cases that will catch you off guard if you don't know what to expect. The first thing to understand is that Fast Refresh isn't magic. It works by tracking module boundaries and re-executing them when changes are detected. The actual implementation uses webpack's hot module replacement under the hood for Create React App, and turbopack for Next.js development builds. When you save a file, here's what happens in practice. The dev server detects the change, identifies which modules are affected, and sends a message to the browser. React then figures out which components can be safely updated without losing state. This works for function components and hooks, but class components have always been problematic.
I ran into a specific issue last month where useState would reset whenever I imported a new utility function, even though the component itself hadn't changed. The root cause was that the import statement was being treated as a module boundary. The workaround was simple: move the utility function to a separate file that doesn't import anything from the component file. That breaks the circular dependency chain.
What Fast Refresh Actually Preserves
Component state from useState, useRef, and useReducer survives updates. Local variables declared inside the component body do not. This distinction matters more than people realize. When you're editing a component that uses multiple hooks, only the state associated with that specific component survives. If you have a parent component with child components, changing the parent will preserve the parent's state but reset all child state. This is by design, not a bug. There's a common misconception that Fast Refresh preserves everything. It doesn't. Global variables, context providers, and anything outside the component function itself will reset. I've seen developers lose entire form states because they stored temporary data in module-level variables, thinking Fast Refresh would save them.
Get the Full Details

Performance Characteristics Worth Knowing
Fast Refresh is fast for small to medium projects, but there's a practical limit. I've worked on monorepos with over 200 components where hot reloading would take 3-5 seconds instead of the usual sub-second updates. The bottleneck is module graph traversal, not the actual React re-render. The turbopack implementation in Next.js 13+ handles this better than webpack, cutting reload times by roughly 60% in my testing. If you're still on webpack and your project has grown significantly, upgrading to the latest Next.js version is probably worth the effort. One counter-intuitive thing: adding more dependencies to a component can actually slow down Fast Refresh. Each dependency adds a node to the module graph that needs to be analyzed. I've seen components with 15+ imports take twice as long to hot reload compared to the same component with 3-4 imports.
When Fast Refresh Fails Completely
There are scenarios where Fast Refresh simply won't work, and you need to know them upfront. First, any file that exports default functions that aren't React components will break the chain. I've wasted hours debugging why my entire app was doing full page reloads when a single utility function was exported as default. The fix is to never use default exports for anything that isn't a component. Second, dynamic imports don't play well with Hot Module Replacement. If you're using React.lazy or import() for code splitting, Fast Refresh will fall back to full page reloads for those chunks. This is a known limitation that the React team hasn't addressed yet.
Third, if you modify exports after the initial module load, Fast Refresh can get confused. I encountered this when refactoring a component to export multiple named functions instead of one default export. The dev server kept showing stale errors until I restarted it entirely. Always restart after major structural changes to your export statements.

Alternative Approaches Worth Considering
If Fast Refresh isn't working well for your setup, there are alternatives. Storybook's live reload is actually more reliable for component isolation, though it doesn't help with full app state preservation. Vite's HMR is generally faster than webpack's implementation, especially for large projects. I switched one of my larger apps from CRA to Vite and saw reload times drop from 2-3 seconds to under 500 milliseconds consistently. For teams that need guaranteed state preservation, the react-refresh-webpack-plugin with custom boundary configurations gives more control, but it requires manual setup. Most projects don't need this level of customization.
Practical Tips That Actually Help
Keep your component files focused. A single file handling 200+ lines of logic will have slower hot reloads than splitting it into smaller, co-located components. This isn't just about Fast Refresh; it's generally better architecture. Avoid importing from the file you're editing. Circular imports are the number one cause of Fast Refresh failures. If ComponentA imports from ComponentB and vice versa, the module boundary detection gets confused. Restructure so imports flow in one direction. Use the React DevTools extension to verify state is actually preserved. I've had cases where Fast Refresh appeared to work but underlying state had silently reset. The DevTools Profiler shows exactly what survived and what didn't during each update.
Disable Fast Refresh temporarily when debugging stubborn issues. Sometimes the hot reload is masking the actual error by re-running code before the previous execution completes. Setting FAST_REFRESH=0 in your environment variables forces full page reloads, which makes errors easier to trace.

The Reality of Using Fast Refresh Long-term
After three years of daily use, my honest assessment is that Fast Refresh is useful but imperfect. It handles 80-90% of typical development workflows without issues. The remaining cases require either restructuring your code or accepting occasional full reloads. The feature has improved significantly since its introduction. Early versions would crash on certain hook patterns. Current implementations are much more stable, but the fundamental limitations remain: it can't preserve non-component state, and it struggles with complex module dependencies. For new projects, Fast Refresh is essentially required. The development experience without it is noticeably worse. For existing projects struggling with it, the incremental refactoring approach usually works better than trying to fix everything at once.