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.
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.
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]; },
});
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.
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.
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 and State API.