# Events and transaction state

Subscribe to the right changes and distinguish source, state slots and temporary storage.

## Choose the state owner

| State | Owner | Document replacement / unmount |
| --- | --- | --- |
| Titles, task values, chart data | Markdown and its codec | Persisted when saved |
| Folding, mapped ranges, transaction-derived values | `defineState()` | Initialized per document |
| Services, cache handles and instance cleanup | Extension storage or `editor.scope` | Released with the instance |
| Dialog or request results | Child `TaskScope` | Released on cancellation/unmount |
| Button hover state | Local Vue / React state | Discarded on unmount |

Persistent fields kept only in storage disappear when reopening a file. Reading preferences should not mutate Markdown merely to refresh UI.

## Select the right notification

`editor.onChange()` observes accepted source changes and suits saving. Extension `onTransaction` includes selections and effects; `onSelectionUpdate` runs only for changed selections. `subscribeState()` observes a bounded derived value instead of recomputing a whole toolbar per keystroke.

~~~ts
const initialHead = editor.state.selection.main.head
const unsubscribe = editor.subscribeState({
  select: state => state.selection.main.head,
  change: (head, previous) => console.debug({ head, previous }),
})
editor.scope.own(unsubscribe)
~~~

Read initial state yourself. Subscriptions do not promise an initial notification. Equality defaults to `Object.is`; provide `equal` for freshly allocated objects, or select stable scalars. Selectors must be pure and must not serialize the whole document or start requests.

## A complete state slot

Import the following module and install `Hints` before reading `hintState`. This transient reading preference intentionally has no undo policy.

### recipes/state.ts

```ts
import { Extension, defineEffect, defineState } from "@quirekit/msfe-core";
/** Explicit view-only preference; it is not a persistent Markdown field. */
export const setHints = defineEffect<boolean>();
/** State resets per document and has no undo policy because this preference is source-neutral. */
export const hintState = defineState({ key: "example.hints",
  /** @returns Default visibility. */ create() { return true; },
  /** @param previous - Accepted or draft value. @param update - Pure transaction inputs. @returns The latest explicitly supplied preference. */
  update(previous, update) { return update.effects(setHints).at(-1) ?? previous; },
});
/** Install the slot before reading it or declaring it as a projection dependency. */
export const Hints = Extension.create({ name: "exampleHints",
  /** @returns State owned by this feature installation. */ addState() { return [hintState]; },
});

```

~~~ts
import { Hints, hintState, setHints } from './recipes/state'

// Include Hints in the editor's initial extensions.
const enabled = editor.getState(hintState)
const result = editor.updateViewState({ effects: [setHints.of(!enabled)] })
~~~

`updateViewState()` permits valid source-neutral effects even in readonly mode. It rejects source edits and reconfiguration. Reducers may run during command speculation. `update.document` provides resulting text; parser-dependent queries belong after acceptance.

Updates preserve the current scroll position by default, which suits explicit expand/collapse actions.
Passive updates such as selection-driven highlights should pass `preserveScroll: false` so pending
keyboard, search, or outline scrolling can finish. This option does not scroll or change the selection itself.

```ts
editor.updateViewState({ effects: [setHints.of(enabled)], preserveScroll: false })
```

## Positions, effects and history

Effects carrying offsets need a `defineEffect(map)` mapping policy. Use `SourceRangeSet` / `mapSourceRange` for ranges, explicitly deciding endpoint affinity and deletion behavior. See [Selection and captured intent](/en/guide/selection).

Only a state definition with `invert` contributes inverse effects. Inverse positions use pre-transaction coordinates. A transient effect without an inverse does not automatically undo with text. `defineMetadata()` stores transaction correlation, not persistent document fields.

## Bound subscription work

Do not mirror the entire document into reactive framework state for toolbar enablement. Observe selection, permission and required slots, then update relevant controls through `can()` and semantic queries. Compare combined toolbar values explicitly. Treat unconfirmed parsing as pending rather than missing syntax.

Declare slots read by a projection in its `states` dependencies. See [Projection layers](/en/extensions/projections) and [State API](/en/api/state).
