# Limitations

> The component shapes that cannot be hot-patched in place, the console line each one prints, and what to do about it.

Eight shapes behave differently under a hot patch, an edit applied to the running page without a reload. Each entry gives the console line it prints, the reason, and the fix; if you are chasing a reload and do not know which one fired, start at [Troubleshooting](/guides/troubleshooting/).

The first three are detected, not guessed, and [`hmr.onIncompatible`](/reference/options/) decides whether the page reloads or only warns. The console lines below are the default `'reload'` wording.

## Standard `accessor` decorators

**What you see**

```text
[lit-plugin] <my-element>: standard accessor decorators — performing full reload.
```

**Why.** Reactive properties declared with standard TC39 decorators close over private slots created fresh on each class evaluation. Accessors copied onto the canonical class, the class the browser registered first, would read the wrong slot. The plugin checks for this before it touches anything.

**What to do.** Use experimental decorators with `experimentalDecorators: true` and `useDefineForClassFields: false`, or declare `static properties`. Both are patched in place. With `onIncompatible: 'warn'` the page stays up and you reload by hand.

## Native `#private` fields

Patched in place by default. In dev the plugin rewrites each `#private` member to a `Symbol.for()` key built from the file, the class and the member name. Instances created before an edit and methods copied from after it use the same key, so private state survives the edit. Builds keep real `#private`.

On Vite 7, esbuild lowers `#private` members of a decorated class to module-level `WeakMap`s and `WeakSet`s before the plugin sees them. The plugin then fetches each of those from a registry keyed by the file and the slot’s name instead, with the same effect. What’s left:

* **Dev drops the brand check.** Reading `obj.#x` from the wrong object returns `undefined` instead of throwing, and `Object.getOwnPropertySymbols()` lists private members. Code that relies on either behaves differently in dev.
* **Decorated private members** such as `@state() accessor #x` leave the whole module unrewritten. They already fall under [standard `accessor` decorators](#standard-accessor-decorators).
* **Lowered classes inside functions**, such as a mixin’s, keep fresh slots on each evaluation, so on Vite 7 their private state fails as before.
* **A mixin applied twice in one chain** shares its private keys across both applications, since every class it returns has the same key.
* **Two same-named classes in one file** are told apart by order. Adding one above the other resets the lower one’s private state once.

With `hmr.privateFields: false`, or a module the rewrite skips, you get the old failure. A copied reactive-property accessor that touches `#private` state throws inside the patch, which is reported as `patching failed` and honours `onIncompatible`:

```text
[lit-plugin] <my-element>: patching failed: Cannot read private member #count from an object whose class did not declare it — performing full reload.
```

A `render()` or method that touches it throws later, in Lit’s asynchronous update, as an uncaught `TypeError`. TypeScript’s `private` modifier compiles to a plain property and avoids both.

## `observedAttributes` changes

**What you see**

```text
[lit-plugin] <my-element> changed observedAttributes; the platform registry can't pick this up — reload recommended.
```

**Why.** The platform snapshots `observedAttributes` when the element is defined, and nothing can change that list afterwards. The patch itself succeeds, so this case never reloads, whatever `onIncompatible` says. The old attribute list stays in effect until you reload.

**What to do.** Reload the page. The message is informational, and the panel’s Components tab keeps a record of it.

## Mixed exports

**What you see.** The component updates, and other values exported from the same module look stale in modules that imported them.

**Why.** A module exporting a component and other values is self-accepting: it tells Vite it can apply its own updates, so the edit stops there. Importers keep the bindings they already read until they re-execute for their own reasons. Class exports stay correct, because the canonical class object is patched in place.

**What to do.** Move shared non-class exports into their own module.

## Module-level state

**What you see.** A counter, cache, or signal declared at the top level of an edited module resets on every edit.

**Why.** A hot patch re-executes the edited module, which re-runs its top-level code. Only the component class survives, not the module’s own variables.

**What to do.** Keep shared signals, context keys, and caches in a module you do not edit in the same loop. See [Ecosystem libraries](/guides/ecosystem/).

## Children of an edited template

**What you see.** After a parent edit, a child element rendered in the edited template literal keeps its count, but a reference you held to it is stale, and listeners or controllers it set up are new. Less often:

* An edit that removes one child and adds another of the same tag in the same shadow root hands the removed child’s state to the added one.
* A value set through an element directive such as `spread()` reverts to the old element’s value.
* The old and new element share the same object or array for a carried property, so mutating one mutates the other’s.
* Under `'reuse'`, a `@query(…, true)` cached during the edit still points at the discarded element.

**Why.** Lit clones fresh DOM for an edited template, so every child in it is a new element. With the default `hmr.childState: 'transfer'`, the plugin pairs old and new children by tag and order within their shadow root and copies reactive properties and `#private` fields across. It skips values the template sets through attributes and property, boolean or attribute bindings, but it can’t see what a directive sets. It copies references, not deep clones.

**What to do.** Move the part you edit often into its own template literal, returned from a helper method, so the children’s template never changes. For a child without bindings, `hmr.childState: 'reuse'` puts the original element back. `'reset'` turns the transfer off. See [`hmr.childState`](/reference/options/#hmrchildstate).

## Interning map growth

**What you see.** Nothing. Memory use creeps up across a long dev session.

**Why.** Template interning reuses the same template object for identical template text, so Lit sees an unchanged template as unchanged. The map is page global, and stale versions of edited templates stay in it.

**What to do.** Nothing, in most sessions. Growth is bounded by how many edits you make, and any full reload clears it.

## Rolldown full-bundle dev mode

**What you see.** The HMR features do not apply.

**Why.** The plugin targets the standard Vite dev server pipeline, where each module is served on its own and re-executed individually.

**What to do.** Use the standard dev server. There is no supported configuration of the full-bundle mode.

## Related

[HMR that keeps state](/guides/hmr/)What a patch does preserve, and the options around it.

[How HMR works](/concepts/how-hmr-works/)Why these specific shapes are the hard ones.
