How to Preserve State with Hot Module Replacement

Hot Module Replacement (HMR) accelerates web development by updating application code in real time without triggering a full page refresh. However, preserving application state across these module updates requires deliberate architectural patterns, proper API integration, and framework-specific considerations. This guide explores the technical strategies engineers use to build reliable, state-preserving HMR workflows, ranging from leveraging native bundler APIs to decoupling data stores from UI components.

Leverage Low-Level HMR Lifecycle APIs

Modern bundlers such as Vite, Webpack, and Turbopack expose lifecycle hooks through import.meta.hot (ESM) or module.hot (CommonJS). These interfaces allow developers to manually serialize state before a module is replaced and rehydrate it immediately after:

  • hot.dispose(callback) / module.hot.dispose(callback): Executes right before the current module is torn down. Use this callback to attach current state to a persistent transfer object, typically hot.data.
  • hot.data: An object preserved across module updates. When a newly updated module executes, it can read from import.meta.hot.data to restore the previously recorded state instead of initializing with default values.
let count = import.meta.hot?.data?.count ?? 0;

export function increment() {
  count++;
}

if (import.meta.hot) {
  import.meta.hot.dispose((data) => {
    data.count = count;
  });
}

Externalize State from the UI Tree

Coupling state management directly to component render trees often leads to state loss when a component file is modified. Moving state out of component definitions isolates it from the reload boundary:

  • Singleton Stores: State libraries like Zustand, Redux, Pinia, or Nano Stores hold data in memory outside the UI component tree. When a component file updates, the bundler re-renders only the view layer while the external store remains untouched in memory.
  • Module Boundary Definitions: Ensure the store definition itself is not in the same file as the component consuming it. If the store definition is re-evaluated, its internal state resets. Placing stores in separate utility files ensures only the consuming UI module re-executes on code edits.

Adhere to Framework-Specific Fast Refresh Rules

Frameworks like React (React Refresh) and Vue (Vue Loader/Vite Plugin Vue) have specialized HMR engines designed to preserve local component state (such as useState or ref), but they require code to adhere to strict structural constraints:

  • Name Components Consistently: React Fast Refresh requires components to use PascalCase naming and be declared at the module root. Anonymous components, arrow functions assigned without a displayName, or higher-order components defined inline cause the engine to fall back to a full re-mount, discarding state.
  • Export Component Only: Do not export non-component utilities, constants, or classes from the same file as a React component. Doing so triggers a cascade that marks the module as non-refreshable, forcing a re-mount.
  • Keep Hooks at the Top Level: Dynamically reordering or conditionally calling hooks during editing alters the hook index, invalidating the component's internal state array and causing a reset.

Isolate Side Effects and Subscriptions

When an updated module runs, duplicate event listeners, timers, or WebSocket connections can corrupt state:

  • Tear Down Observers: Always clean up global listeners (window.addEventListener) inside dispose handlers or within the unmount lifecycle of the framework.
  • Idempotent Initialization: Guard global setups using existing flags (e.g., checking if a global instance already exists before re-instantiating) to avoid re-running side effects that reset in-memory data caches.

Implement Dev-Only Storage Fallbacks

For complex workflows involving deep nested trees or multi-step forms, relying solely on in-memory preservation may not be enough during severe syntax error recoveries.

  • LocalStorage Sync: Synchronize transient state to sessionStorage or localStorage during development mode via custom middleware.
  • URL State Management: Encode critical view states, filter parameters, and active tabs into URL query parameters. This guarantees persistence across both hot updates and unavoidable hard reloads caused by critical errors.