Events & effects
Use notifications when something outside the view needs to observe an occurrence.
An event declaration describes a public name and payload.
It does not emit events or validate payloads at runtime.
Choose one producer per event:
- A native source event represents an occurrence, including reset-at-zero.
- An effect derives a notification by comparing accepted source snapshots.
One counter, two meanings
Section titled “One counter, two meanings”import { igniteCore } from "ignite-element/xstate";import { assign, createActor, emit, setup } from "xstate";
export const counterMachine = setup({ types: { context: {} as { count: number }, events: {} as { type: "INCREMENT" } | { type: "RESET" }, emitted: {} as { type: "counterReset"; count: number }, },}).createMachine({ context: { count: 0 }, on: { INCREMENT: { actions: assign({ count: ({ context }) => context.count + 1 }), }, RESET: { actions: [ assign({ count: 0 }), emit(({ context }) => ({ type: "counterReset", count: context.count })), ], }, },});
// The application creates, starts and eventually stops its borrowed source.export const counterActor = createActor(counterMachine).start();export const core = igniteCore({ source: counterActor, states: (snapshot) => ({ count: snapshot.context.count }), commands: ({ source: actor }) => ({ increment: () => actor.send({ type: "INCREMENT" }), reset: () => actor.send({ type: "RESET" }), }), events: (event) => ({ countChanged: event<{ count: number }>(), counterReset: event<{ count: number }>(), }), effects: ({ select, emit }) => { const count = select((snapshot) => snapshot.context.count); if (count.changed) emit({ type: "countChanged", count: count.current }); },});
// Subscribe before issuing commands. execute().events is a capture window,// not correlation with a durable business operation.export const notifications: { type: string; count: number }[] = [];core.on("countChanged", (event) => notifications.push(event));core.on("counterReset", (event) => notifications.push(event));
// When this core is permanently discarded, dispose it.// The actor owner stops counterActor when nothing else needs it.// Terminal disposal owns the two subscriptions; no redundant unsubscribe pair.counterReset comes from the source; countChanged comes from a state comparison.
Do not mirror native occurrences in effects.
Headless listeners may infer native source emissions without declarations.
DOM forwarding and React web callbacks expose declared public names.
See the runnable event counter view.
DOM events use the public name and flat detail; React web callbacks receive that detail directly.
Headless core.on(name, handler) receives { type, ...fields }.
execute().events is an observation window, not a durable or command-correlated business receipt.
Delivery and ownership
Section titled “Delivery and ownership”Effect activation and lifetime
Section titled “Effect activation and lifetime”Effects evaluate once per delivered source update per core/source instance.
The initial activation establishes a baseline without calling the effect.
Each later notification queues one synchronous callback that returns void.
select(...).changed compares values with Object.is.
Intermediate updates are not combined.
A shared evaluator starts on legitimate runtime use or a committed subscription or element connection.
Constructor preparation and render-time reads do not activate it.
Once activated, it observes the source until core.dispose(), even with no connected views.
Reconnecting a view does not reset the baseline.
Each isolated source instance has its own evaluator and baseline, released with that instance.
Separate cores remain independent even when they borrow the same actor.
Effect timing and errors
Section titled “Effect timing and errors”Delivery is queued after source processing.
It does not guarantee that a renderer update or framework commit has completed.
Synchronous Ignite rendering can complete before the microtask runs.
A framework view may still be waiting to commit.
Core-owned effect failures use console.error once per failed evaluation.
They do not use an element’s handleError or onError hook.
An asynchronous return reports the synchronous-void violation and any promise rejection.
Native event delivery
Section titled “Native event delivery”DOM forwarding uses the names declared in the core’s public event map.
The event’s type becomes the DOM event name.
The remaining flat payload becomes detail, with bubbles and composed set to true.
Headless listeners can also observe undeclared native names.
Forwarding a DOM event does not feed it back into core.on or execution capture.
Additional views receive the occurrence without producing another source event.
Two identical native resets remain two occurrences.
Events from different hosts can bubble to the same ancestor.
Ignite does not suppress propagation or deduplicate those deliveries.
Subscribe before work starts
Section titled “Subscribe before work starts”Subscribe before starting work whose events matter.
Events are not replayed, and ordering across separate event streams is not guaranteed.
An event emitted before a React wrapper attaches its listeners may not reach the callback.
Ignite does not restart a borrowed source to replay startup events.
For a queued effect, listener eligibility is checked when the effect emits.
A listener that subscribes before that emission can receive it.
Subscribing after emission does not replay the event.
Disconnecting or unsubscribing before delivery removes that recipient.
Terminal core disposal discards pending evaluations and later emissions.
Disconnect and disposal
Section titled “Disconnect and disposal”A true element disconnect removes its forwarding subscription.
Reconnect creates a fresh subscription; a same-tick move retains the existing one.
Isolated source instances remain separate.
Disconnect never shuts down an application-owned source.
Call core.dispose() when the owning feature or session permanently discards the core.
The owner of counterActor stops it when nothing else needs that actor.
See terminal disposal for cleanup that preserves source shutdown if disposal throws.
Unsubscribe an individual listener when its consumer ends before the core does.
Unmounting a view does not necessarily end its owning feature or session.
Example: one effect, two hosts
Section titled “Example: one effect, two hosts”For two connected app-counter elements sharing the core above, one increment evaluates the effect once.
That evaluation produces one countChanged occurrence.
Each host dispatches its own DOM event, so a shared ancestor sees two deliveries.
Each core.on registration receives one delivery.
Adding a useIgnite(core) consumer does not add an evaluator.
DOM consumers work without a headless listener, and headless listeners work without DOM hosts.
View-specific work
Section titled “View-specific work”Use framework lifecycle facilities for work tied to a mounted view.
Focus, layout, canvas and native view resources remain framework-owned.
Persistence, retry and durable confirmation remain source/application-owned.
Ignite effects derive notifications from source updates; they do not provide application-wide exactly-once business execution.
One production rule per public event
Section titled “One production rule per public event”A precise native emitted union reserves its names: declaring counterReset is valid, but calling effects.emit({ type: 'counterReset', ... }) is a type error.
Public payload declarations must accept the actual native payload without its discriminator.
Incoming command names alone are not reserved.
Broad strings, erased generics, any, and missing channel evidence cannot provide complete static protection.
See Sources for Redux and MobX examples.
Duplicate producer warnings
Section titled “Duplicate producer warnings”In development, observing the same name from both origins warns once for that core/source ownership lifetime.
Separate isolated sources and unrelated cores, same-origin repeats, and multiple projection recipients do not conflict.
Detection requires both producers to run while observation is active.
It adds no source or monitoring subscription.
It cannot prevent or retract earlier delivery.
Warnings contain names/origins, not payloads or snapshots.
Neither development nor production chooses a winner, throws, drops, or deduplicates events.