Volume I index
Project Tooling and the First Milestone

Vite Dev Server and HMR

Ishtmeet Singh @ishtms/July 14, 2026/9 min read
#react#vite#hmr#fast-refresh#development

The development server turns a saved source edit into browser feedback.

Three programs take part when you edit a component.

  1. The editor writes a changed file to disk.
  2. Vite detects the file and sends a module update to the browser.
  3. React Fast Refresh decides how React component code should be replaced.

The browser may update without reloading the document, but that outcome is conditional. It is not the same as React rendering after an application state update.

The Development Server Is Not a Production Server

Start the project.

bash
npm run dev

Vite opens an HTTP server for local development. It serves index.html, transforms requested source modules, resolves package imports, and maintains a connection used to announce updates.

During development, the browser can request source modules individually.

text
index.html
  /src/main.jsx
  /src/App.jsx
  /src/ChapterCard.jsx
  /src/index.css

This is one reason startup can be quick. Vite does not have to emit the final production bundle before the browser can load the first page. It transforms modules as they are requested and pre-bundles selected dependencies for development efficiency.

Production uses another path. vite build creates optimized output ahead of deployment. The local dev server should not be exposed as the deployed application server.

Save One Component Edit

Begin with state that is easy to observe.

jsx
function Counter() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount((n) => n + 1)}>{count}</button>;
}

Click until the button shows 3. Then change only its surrounding text.

jsx
return <button onClick={() => setCount((n) => n + 1)}>Count: {count}</button>;

In a normal Fast Refresh update, the new text appears and the count remains 3.

This result contains two different kinds of continuity.

  • Vite kept the current browser document alive while replacing an accepted module.
  • React kept the compatible component instance and its local state.

Vite alone does not understand React Hook state. The React plugin supplies the Fast Refresh integration that lets React decide whether the component family remains compatible.

HMR and Fast Refresh Are Different Layers

Hot Module Replacement, or HMR, is Vite's system for replacing modules while an application is running. CSS can use HMR without React.

css
.counter {
  color: #61dafb;
}

Save a color change and Vite can update the stylesheet without recreating the page.

React Fast Refresh is specific to React component modules. It registers component exports, detects compatible edits, and asks React to refresh affected trees.

text
saved file
  -> Vite finds importers
  -> Vite sends HMR update
  -> React plugin identifies component exports
  -> Fast Refresh replaces compatible component code
  -> React renders affected components

Why State Can Survive an Edit

React can preserve local state when the edited export still represents the same compatible component family and Hook order remains valid.

Change a label or add ordinary rendering logic and preservation often works.

jsx
const label = count === 1 ? "lesson" : "lessons";

Change the Hook order and preservation would be unsafe.

jsx
function Counter({ detailed }) {
  if (detailed) {
    const [label, setLabel] = useState("Lessons");
  }

  const [count, setCount] = useState(0);
  // ...
}

This code also violates the Rules of Hooks and must be repaired. Fast Refresh does not make conditional Hooks acceptable.

State can reset after edits to a component's Hooks. Adding or removing a Hook may produce a refresh with fresh state so React can establish a valid Hook list.

That reset is a development event, not proof of production behavior. A deployed user is not editing component source.

Module Exports Affect Refresh Boundaries

Fast Refresh works best when a module exports React components. Consider a component module that also exports a value used by non-React code.

jsx
export const chapterLimit = 5;

export default function ChapterList() {
  // ...
}

An edit can affect importers that are outside the React refresh path. Depending on the import graph, Vite may need to invalidate more modules or reload the page.

Keep reusable constants in their own module when they have non-component consumers.

js
// chapter-config.js
export const chapterLimit = 5;
jsx
// ChapterList.jsx
import { chapterLimit } from "./chapter-config.js";

This is not a rule that every constant needs a file. It is a reason to keep component modules centered on component exports once values gain unrelated consumers.

A Full Reload Has Different Consequences

A full reload requests index.html again and starts the client application from the beginning.

text
browser discards current document
browser requests entry document
module graph loads again
createRoot runs again
component state initializes again

In-memory state resets unless it was saved somewhere outside the discarded page, such as a server, URL, or browser storage.

Some edits require this. Changing index.html, changing a module outside an accepted HMR path, or introducing an unrecoverable module error can lead to reload behavior.

You can always perform a manual reload. Doing so is a useful test after a sequence of Fast Refresh updates. It proves that the application works from a clean start instead of relying on preserved development state.

Error Recovery Is Part of the Loop

Introduce a temporary syntax error.

jsx
return <button>{count</button>;

Vite cannot transform the module, so it cannot send valid replacement code. The last valid module may remain behind the error overlay. After the syntax is repaired, Vite sends another update.

State may survive some error corrections and reset after others. The priority is a correct clean start, not preservation through every broken intermediate file.

The terminal and overlay report transform failures. A runtime exception after successful transformation appears in browser execution and React reporting. First determine whether Vite produced a valid update at all.

Dependency Optimization Is a Development Step

Browser source can import a package by name.

jsx
import { useState } from "react";

Browsers do not resolve bare npm package names from node_modules on their own. Vite resolves those imports and can pre-bundle dependencies into browser-friendly modules. Vite 8 uses Rolldown for its production build and dependency optimization path.

When dependency metadata changes, Vite may optimize dependencies again. Installing a package while the dev server is already running can require a restart.

bash
npm install date-fns

Stop and restart Vite if the running server does not pick up the new graph.

bash
npm run dev

Do not reach for cache deletion on every source error. Dependency optimization is relevant when installed packages or their resolution changed, not when an h1 tag is missing.

Environment Values Are Read at Server Startup

Vite exposes selected client environment values through import.meta.env. Client-exposed custom names use the VITE_ prefix by default.

env
VITE_API_ORIGIN=https://api.example.com
js
const apiOrigin = import.meta.env.VITE_API_ORIGIN;

Vite replaces these values in client code. A value included in a client bundle is visible to users who can load that bundle.

Environment files are loaded when Vite starts. Restart the development server after changing them so the new values are read consistently.

This project does not need an API credential. The example demonstrates why an environment change requires a server restart instead of an ordinary source refresh.

Network Access and Ports

The printed URL normally uses localhost and port 5173.

text
http://localhost:5173/

If that port is occupied, Vite can choose another. Always open the URL printed by the current terminal rather than relying on an old bookmark.

The browser loads the page from the development server origin. Requests to another origin are subject to browser cross-origin rules. Vite's development proxy can forward selected paths, but a proxy is not needed for this local-data milestone.

Adding a proxy before an API exists would introduce configuration that has nothing to serve.

Test the Three Update Paths

Use the counter to observe three distinct outcomes.

  1. Change a CSS color. The style updates through HMR.
  2. Change component text. Fast Refresh normally preserves the count.
  3. Reload the browser. The component state starts from zero.

Then make sure the app behaves correctly after the third path. That is the clean-start behavior a reader or reviewer can reproduce.

What to Record in a Bug Report

When local update behavior fails, record concrete facts.

  • Vite version from npm ls vite
  • React plugin version from npm ls @vitejs/plugin-react
  • changed file and its exports
  • whether the terminal reported HMR, invalidation, or an error
  • whether the browser refreshed the module or reloaded the document
  • whether a manual reload repairs the visible problem

"Hot reload is broken" collapses several systems into one sentence. The facts above isolate the layer that behaved unexpectedly.

Source Notes