---
title: Hot reload
description: Save a file and the page changes in place, keeping what you were doing.
section: Frontend
order: 9
---

# Hot reload

<p class="lead"><code>sluurp serve</code> watches the app folder. Save a file and every open page updates in place, keeping its state: counters keep their count, dialogs stay open, inputs keep their text. No setup, no bundler.</p>

```sh title="Terminal"
sluurp serve --public ./app
```

## Swapped in place

The saved module replaces the old one and the page keeps its state. Each case below is covered by the hot lab, a test app that makes every kind of edit and checks state survives:

- **Components:** local state, module-level signals, open dialogs, keyed lists, child components in other files.
- **Plain modules:** `.ts` files imported by components, nested imports, circular imports.
- **Data and assets:** `.json`, `import.meta.glob` (including added/deleted files), `?raw` and `?url` imports, `import.meta.env` from `.env`.
- **Styles:** stylesheets and CSS modules, swapped without re-rendering.
- **Workers:** TypeScript workers, restarted with the new code.

```tsx title="islands/counter.tsx"
import { signal } from "sluurp/reactive";

export default function Counter() {
  const count = signal(0);
  // Change this label and save: the count stays where it was.
  return <button onClick={() => count.set(count() + 1)}>Clicked {count} times</button>;
}
```

## Re-rendered or reloaded

- **Markdown pages, layouts and server routes** are re-rendered and patched into the page, without a reload.
- **Modules with top-level side effects** (not just definitions) trigger a full reload, since running them twice would repeat the effect.

## When something breaks

Compile errors and uncaught exceptions show in the dev overlay with the source line and a link to open it in your editor. Non-fatal problems (a failed request, a `console.error`) are counted in a corner badge instead. Fix and save; the overlay clears itself.

## Pre-bundling and caching

Your own files are served individually so each can be hot-swapped. Libraries aren't: at startup Sluurp pre-bundles every library your app imports (built-ins like `sluurp/reactive` and the UI kit, plus vendored packages), like Vite does.

- **No duplicates.** It's a single build with code splitting, so shared code is loaded once.
- **Only what you use.** Only the names your code imports are exported. Imports used only by lazily loaded screens load with those screens.
- **Immutable caching.** Bundles and your modules are served at content-hashed URLs (`/session.ts?v=3f9a1c07`) and cached forever, so a reload only fetches what changed.
- **Automatic lazy loading.** A component rendered only conditionally (`open() ? <Invoice/> : ""`, `ready && <Chart/>`) and never used as a value is fetched the first time it renders, with its exclusive dependencies. You write a normal import; the server rewrites it. Components every page renders stay eager, so nothing pops in late.

Importing something new from a library rebuilds the bundle and reloads the page (about a second). To hot-swap edits to a library itself, serve with `SLUURP_PREBUNDLE=off`.

## Checks on every edit

When a file changes, Sluurp checks it for two mistakes and prints them in the server log and the browser console:

- a component used in JSX that nothing declares (`<Buton>` for `<Button>`);
- an import nothing uses.

```
src/app.tsx:12:8 Buton is not defined
```

There's nothing to set up and no rules to configure: these checks come from the same parse that compiles the file.

## In production

Hot reload is dev-only. With `--no-hot-reload` or a published bundle, nothing is watched and no reload script is injected.

The hot lab lives in `e2e/hot-lab`, one card per case; its browser test edits each and checks state is preserved.
