Skip to content

Hot Module Replacement

Sygnal has built-in HMR support that preserves application state across code changes.

If you’re using the Sygnal Vite plugin, HMR is wired automatically. No extra code needed:

vite.config.js
import sygnal from 'sygnal/vite'
export default defineConfig({ plugins: [sygnal()] })
// src/main.js — just run, no HMR boilerplate
import { run } from 'sygnal'
import App from './App.jsx'
run(App)

The plugin detects the run() call and injects import.meta.hot.accept / dispose at build time.

If you’re not using the plugin, wire HMR yourself:

src/main.js
import { run } from 'sygnal'
import RootComponent from './RootComponent.jsx'
const { hmr, dispose } = run(RootComponent)
if (import.meta.hot) {
import.meta.hot.accept('./RootComponent.jsx', hmr)
import.meta.hot.dispose(dispose)
}
import { run } from 'sygnal'
import RootComponent from './RootComponent'
const { hmr, dispose } = run(RootComponent)
if (module.hot) {
module.hot.accept('./RootComponent', hmr)
module.hot.dispose(dispose)
}
  1. When a file changes, the bundler triggers the accept callback
  2. Sygnal captures the current application state
  3. The old application instance is disposed
  4. A new instance is created with the updated code
  5. The captured state is restored into the new instance

State is preserved across reloads via window.__SYGNAL_HMR_PERSISTED_STATE.

For TypeScript projects using manual HMR, you may need to cast the module:

import { run } from 'sygnal'
import App from './app'
const { hmr } = run(App)
if (import.meta.hot) {
import.meta.hot.accept('./app', (mod) => {
hmr((mod as { default?: typeof App })?.default ?? App)
})
}

This is not necessary when using the Vite plugin.