Skip to main content
ASoc
Tutorial

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.

The ASoc Team8 min read

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:

  1. 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.
  2. 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.

MeasurementResult
Dev server ready520 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 route0.92 s
Edit a heading in src/components/organisms/PricingTiers.tsx, time until the served HTML contains it1.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 CSSNew 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

SymptomLikely causeFix
Saving does nothing, manual reload shows the changeWatcher event missed or push to browser failedDisable editor safe-write; check bind-mount watching
Saving does nothing, manual reload does not eitherDev server stopped, or you are editing a different checkoutCheck the terminal; confirm the path
Page fully reloads on every editFile exports non-components, or a class componentMove helpers and constants out of the file
Form state clears after editHook order or count changedExpected; reload once and continue
Console shows a blocked-script error in dev onlyCSP without 'unsafe-eval' in developmentGate 'unsafe-eval' on NODE_ENV === "development"
New Tailwind class has no effectBrand-new @theme tokenRestart npm run dev
First load takes ~10 sCold compile of the routeWait once; later edits are fast
Works in dev, differs in productionDev and production evaluate modules differentlyTest 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.

Keep reading

Tutorial8 min read

React JSX: The Rules That Only Break the Production Build

JSX compiles to function calls, not HTML. Where the two diverge, why a lowercase SVG tag survives dev and fails next build, and the key rule behind phantom state bugs.

Read more
Tutorial10 min read

React Landing Pages: Half the HTML Isn't the Page

This storefront's home page prerenders to 436 KB, and 52.1% of that is a serialized copy of the render, not the page. What actually reaches a crawler before any JavaScript runs.

Read more