React Hot Reload Not Working: Five Causes, Measured
Measured on Next 16.2.9: edits reach the page in 0.35–1.5 s. The five causes of broken React hot reload, including the CSP one tutorials skip.
When React hot reload stops working, the cause is almost always one of five things: the dev server is not watching your file, the file exports something that is not a component, the component's state is being deliberately reset, a security header is blocking the dev runtime, or a build-tool cache is stale. On Next.js 16.2.9 with Turbopack, this repository measured an edit reaching the rendered page in 0.35 to 1.5 seconds, so anything slower than a couple of seconds is a symptom worth chasing.
The short answer
"Hot reload" is two different mechanisms, and people mix them up:
- Fast Refresh is React's module-level patching. Edit a component file and the framework swaps the new function in while keeping state. It works only when the edited file exports React components and nothing else it cannot safely re-run.
- Full reload is the fallback. When Fast Refresh cannot patch a module, the page reloads. Nothing is "broken" — you just lost the hot part.
Old advice about react-hot-loader and module.hot.accept is for webpack setups from before Fast Refresh shipped inside React tooling. A current Next.js project needs none of that, and adding it is a common way to break a working setup.
What this repo measured, today
Everything below comes from running next dev (Next 16.2.9, Turbopack, React 19.2.4) against this storefront on 2026-10-13, with a dummy Supabase URL so the proxy could run.
| Measurement | Result |
|---|---|
| Dev server ready | 520 ms |
First request to /pricing (cold compile) | 10.8 s (Next compile 8.4 s, src/proxy.ts 326 ms, application code 2.1 s) |
| Second request to the same route | 0.92 s |
Edit a heading in src/components/organisms/PricingTiers.tsx, time until the served HTML contains it | 1.48 s, 0.58 s, 0.35 s (three consecutive edits) |
Edit --color-primary in src/app/globals.css, then re-fetch the page's CSS | New value present in the stylesheet within 6 s |
Two honest caveats. The edit timings poll the server-rendered HTML for the new text, so they measure the compile-and-serve half, not the browser repaint. And they are three samples on one machine — treat them as an order of magnitude, not a benchmark.
The shape is the useful part: the first compile of a route is the slow one by a wide margin, and every edit after it is sub-two-seconds. If your first edit feels dead for ten seconds, that is a compile, not a failure.
The five causes, in the order to check them
1. The edit never reached the watcher
The dev server only reacts to file events. Two setups break that:
- Editors with "safe write" save to a temporary file and rename it. Some watchers see a delete and a create instead of a change. Turn that setting off in your editor if saves are silently ignored.
- Bind mounts and network drives. Edits made on the host do not always raise file events inside a container. This repo's Docker notes are in Next.js Docker; the short version is that if editing the file inside the container works and editing it on the host does not, it is a watching problem, not a React problem.
A one-minute test that separates "watcher" from "React": add a visible string to a Server Component and reload by hand. If a manual reload shows it, the build saw the change and only the push to the browser failed.
2. The file exports more than components
Fast Refresh only patches a module whose exports are all components. Export a constant, a helper or a type-bearing object from the same file and a hot patch can degrade into a full reload, which also wipes component state.
The convention that avoids this in this codebase is already structural. Under the Atomic Design layout in src/components/, an atom, molecule or organism file exports one component. Content arrays live in src/data/, helpers in src/lib/. We did not set that up for hot reload, but it is the reason a content edit and a component edit behave differently:
src/data/pricing.tsx # edit -> consumers re-render
src/components/organisms/ # edit -> that component patches in place
If a single file mixes the two, split it before you debug anything else.
3. State resets, and that is the design
Fast Refresh keeps useState values, but it resets state when you edit a component that is a class, or when you change the shape of the hooks (add, remove or reorder them). Hooks are matched by call order, so a changed order cannot be matched safely. Seeing a form empty itself after you edit its markup is expected for the second case, not a regression.
4. A security header is blocking the dev runtime
This is the one the usual tutorials skip, and the one this repo's config handles explicitly. Development builds evaluate modules in ways production does not, so a strict Content-Security-Policy that is correct for production can stop the dev runtime. The handling is in next.config.ts:
// Next.js/Turbopack dev (Fast Refresh) evaluates modules via eval; production
// (`next start`) is eval-free. Allow 'unsafe-eval' ONLY in development so local
// HMR works without weakening the shipped production policy.
const scriptSrcEval =
process.env.NODE_ENV === "development" ? " 'unsafe-eval'" : "";
and it is spliced into the policy as `script-src 'self' 'unsafe-inline'${scriptSrcEval} …`. The point of writing it this way is the asymmetry: the dev server gets 'unsafe-eval', the production header does not. If you set a CSP and hot reload dies with the console complaining about a blocked script, loosen it for development only, never in the shipped value. For why that policy contains 'unsafe-inline' at all, the comment block above it in the same file records the trade-off.
5. Stale tokens and caches
Our own CLAUDE.md says that Tailwind v4 with Turbopack does not hot-reload @theme changes and that you should restart npm run dev after editing tokens. We re-tested that on 16.2.9 for this post: changing --color-primary in src/app/globals.css and re-fetching the stylesheet returned the new colour within six seconds, with no restart. So on this version an existing token edit did propagate.
What we did not test is adding a brand-new token and using a utility generated from it immediately, which is the case the note was written about. Keep the restart habit for that case, because it costs two seconds and the failure is silent — the class simply produces no CSS. If you want the broader picture of how utilities get generated, Tailwind hover walks through one.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Saving does nothing, manual reload shows the change | Watcher event missed or push to browser failed | Disable editor safe-write; check bind-mount watching |
| Saving does nothing, manual reload does not either | Dev server stopped, or you are editing a different checkout | Check the terminal; confirm the path |
| Page fully reloads on every edit | File exports non-components, or a class component | Move helpers and constants out of the file |
| Form state clears after edit | Hook order or count changed | Expected; reload once and continue |
| Console shows a blocked-script error in dev only | CSP without 'unsafe-eval' in development | Gate 'unsafe-eval' on NODE_ENV === "development" |
| New Tailwind class has no effect | Brand-new @theme token | Restart npm run dev |
| First load takes ~10 s | Cold compile of the route | Wait once; later edits are fast |
| Works in dev, differs in production | Dev and production evaluate modules differently | Test with npm run build and npm run start |
Frequently asked questions
Do I need react-hot-loader? No. Fast Refresh replaced it and is built into the Next.js dev server. Installing the old package alongside is a way to create a problem, not fix one.
Why does my state survive some edits and reset on others? Fast Refresh preserves hook state when the hook calls keep the same order and count, and resets it for class components or changed hook shapes.
Is it safe to add 'unsafe-eval' to my CSP?
Only for the development server. Gate it on NODE_ENV === "development" so the header you ship never contains it; the pattern is in next.config.ts above.
Why is the first page load so slow in dev?
The route compiles on first request. In our run /pricing took 10.8 s cold and 0.92 s on the second request, and each following edit appeared in under two seconds.
Templates in this post
ASoc Quill (a lightweight blog template with a featured post and story feed), ASoc Rally (an AI CRM marketing site with a pipeline preview) and ASoc Rank (an SEO-audit SaaS site with pricing and FAQ) all keep content arrays in a data folder and one component per file, so edits patch in place instead of triggering full reloads.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For environment files that change what the dev server loads, see Next.js env.
