Views
The renderer turns source-derived values into a view.
Use ctx to access those values and source-directed commands without duplicating state in the view.
Ignite JSX
Section titled “Ignite JSX”Call core('my-component', ctx => ...), as in the light switch.
JSX files use .tsx and jsxImportSource: "ignite-element/jsx".
A source-free layout can use igniteCore() from ignite-element; it returns only a registrar, without the source-backed runtime methods.
Install react and react-dom for a web application.
Use React’s JSX runtime in React files and the Ignite pragma in files that register Ignite JSX components.
Two views, one source
Section titled “Two views, one source”Increment either counter or edit its label.
Both views update because they use the same core and source.
Connect the views
Section titled “Connect the views”Create the shared core outside rendering, then call const ctx = useIgnite(core).
Save these modules together:
import { igniteCore } from "ignite-element/xstate";import { assign, createActor, createMachine } from "xstate";
const counterMachine = createMachine({ types: {} as { events: | { type: "INCREMENT" } | { type: "DECREMENT" } | { type: "LABEL"; value: string }; }, context: { count: 0, label: "Visitors" }, on: { INCREMENT: { actions: assign({ count: ({ context }) => context.count + 1 }), }, DECREMENT: { actions: assign({ count: ({ context }) => context.count - 1 }), }, LABEL: { actions: assign({ label: ({ event }) => event.value }), }, },});
// The application starts this shared actor and owns its shutdown.export const source = createActor(counterMachine).start();
export const core = igniteCore({ source, states: (snapshot) => ({ count: snapshot.context.count, label: snapshot.context.label, canIncrement: snapshot.can({ type: "INCREMENT" }), }), commands: ({ source: actor }) => ({ increment: () => actor.send({ type: "INCREMENT" }), decrement: () => actor.send({ type: "DECREMENT" }), setLabel: (value: string) => actor.send({ type: "LABEL", value }), }),});/** @jsxImportSource react */import { useIgnite } from "ignite-element/react";import { core } from "./counter-core";import "./counters.css";
export function Counter() { const ctx = useIgnite(core); return ( <section className="counter-card" aria-label="Shared counter"> <p> {ctx.label}: <output aria-label="Count">{ctx.count}</output> </p> <div className="counter-controls"> <button type="button" onClick={() => ctx.decrement()}> Decrement </button> <button type="button" disabled={!ctx.canIncrement} onClick={() => ctx.increment()} > Increment </button> </div> <label> Counter label <input value={ctx.label} onChange={(event) => ctx.setLabel(event.target.value)} /> </label> </section> );}export function SharedCounters() { return ( <div className="ignite-counter-demo"> <div className="counter-grid"> <Counter /> <Counter /> </div> </div> );}Render <SharedCounters /> in your React application.
counters.css — styles for both demos
.ignite-counter-demo { color: var(--sl-color-text, CanvasText); font-family: system-ui, sans-serif;}
.ignite-counter-demo .counter-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: 1rem; align-items: stretch;}
.ignite-counter-demo .counter-card { display: grid; gap: 1rem; padding: 1.25rem; border: 1px solid var(--sl-color-gray-4, GrayText); border-radius: 0.5rem; background: var(--sl-color-bg, Canvas);}
.ignite-counter-demo p { margin: 0;}
.ignite-counter-demo output { font-size: 2rem; font-weight: 600; font-variant-numeric: tabular-nums;}
.ignite-counter-demo .counter-controls { display: flex; flex-wrap: wrap; gap: 0.5rem;}
.ignite-counter-demo label { display: grid; gap: 0.375rem;}
.ignite-counter-demo button,.ignite-counter-demo input { box-sizing: border-box; min-width: 0; min-height: 2.75rem; padding: 0.375rem 0.625rem; border: 1px solid var(--sl-color-gray-4, GrayText); border-radius: 0.375rem; background: var(--sl-color-bg, Canvas); color: inherit; font: inherit;}
.ignite-counter-demo button { cursor: pointer;}
.ignite-counter-demo button:hover { background: var(--sl-color-bg-inline-code, ButtonFace);}
.ignite-counter-demo :is(button, input):focus-visible { outline: 2px solid var(--sl-color-text-accent, Highlight); outline-offset: 3px;}
.ignite-counter-demo .event-status { font-size: 1rem; font-weight: 400; display: grid; place-content: center; min-height: 8rem; padding: 1.25rem; border: 1px solid var(--sl-color-gray-4, GrayText); border-radius: 0.5rem; text-align: center; font-variant-numeric: tabular-nums; background: var(--sl-color-bg, Canvas);}
.ignite-counter-demo [data-parity="even"] { color: var(--sl-color-blue-high, #123b70); background: var(--sl-color-blue-low, #e4efff);}
.ignite-counter-demo [data-parity="odd"] { color: var(--sl-color-purple-high, #542175); background: var(--sl-color-purple-low, #f2e8ff);}Shared state and cleanup
Section titled “Shared state and cleanup”This is the runnable React example.
Both views observe the same count and label.
useIgnite(core) automatically unsubscribes when the component unmounts.
No cleanup useEffect is needed in the view.
The core stays available for other views and later mounts.
Call core.dispose() when you permanently discard that core, such as when its owning feature or session ends.
If you supplied an external actor, its owner stops it when nothing else needs it.
See the session cleanup example.
This example passes an existing actor, so the core is ready for useIgnite(core) without a preparation read.
Passing a machine, slice or fresh factory instead gives each hook private state with automatic runtime cleanup on unmount and fresh state on a genuine new mount.
The same synchronous ctx works in React Native.
Construction safety and the separate explicit headless runtime are described in ownership.
React Native
Section titled “React Native”Use the same neutral ignite-element/react hook and native controls.
This is a complete view module using the shared counter-core.ts above.
import { useIgnite } from "ignite-element/react";import { Pressable, Text, View } from "react-native";import { core } from "./counter-core";
export function CounterScreen() { const ctx = useIgnite(core); return ( <View> <Text>{ctx.count}</Text> <Pressable onPress={() => ctx.decrement()}> <Text>Decrement</Text> </Pressable> <Pressable onPress={() => ctx.increment()}> <Text>Increment</Text> </Pressable> </View> );}The native consumer fixture checks real React Native types and its standard Jest host.
That is not device or simulator acceptance.
SSR/hydration and Solid/Vue headless bindings are not implemented.
Browser custom-element interoperability
Section titled “Browser custom-element interoperability”When React intentionally hosts a real element, use igniteReact(handle) from ignite-element/react/web.
React to an emitted event
Section titled “React to an emitted event”Use the custom element’s buttons to change its count.
React receives countChanged through onCountChanged and updates the status beside it.
The odd/even label and color belong to the React view.
Listen in React
Section titled “Listen in React”onCountChanged receives the event’s data directly: { count }.
/** @jsxImportSource react */import { useState } from "react";import { Counter } from "./counter.react";import "../counters.css";
export function WebInterop() { const [count, setCount] = useState<number | null>(null); const parity = count === null ? undefined : count % 2 === 0 ? "even" : "odd";
return ( <section className="ignite-counter-demo" aria-label="Custom-element interoperability" > <div className="counter-grid"> <Counter onCountChanged={({ count }) => setCount(count)} /> <output className="event-status" aria-label="React event status" data-parity={parity} > {count === null ? "Waiting for an event." : `React received: ${count} — ${parity === "even" ? "Even" : "Odd"}`} </output> </div> </section> );}Render <WebInterop /> in your React application.
Wrap the custom element
Section titled “Wrap the custom element”igniteReact creates the React component from the registered element handle.
import { igniteReact } from "ignite-element/react/web";import { counterElement } from "./counter.ignite";
export const Counter = igniteReact(counterElement);Emit the count
Section titled “Emit the count”The element declares countChanged and emits it when the selected count changes.
/** @jsxImportSource ignite-element/jsx */import { igniteCore } from "ignite-element/xstate";import { assign, createMachine } from "xstate";
const counterMachine = createMachine({ context: { count: 0 }, on: { INCREMENT: { actions: assign({ count: ({ context }) => context.count + 1 }), }, DECREMENT: { actions: assign({ count: ({ context }) => context.count - 1 }), }, },});
const counterCore = igniteCore({ source: counterMachine, states: (snapshot) => ({ count: snapshot.context.count }), commands: ({ source }) => ({ increment: () => source.send({ type: "INCREMENT" }), decrement: () => source.send({ type: "DECREMENT" }), }), events: (event) => ({ countChanged: event<{ count: number }>(), }), effects: ({ emit, select }) => { const count = select((snapshot) => snapshot.context.count); if (count.changed) emit({ type: "countChanged", count: count.current }); },});
export const counterElement = counterCore("react-demo-counter", (ctx) => ( <section class="counter-card" aria-label="Ignite counter"> <link rel="stylesheet" href={new URL("./counter.css", import.meta.url).href} /> <p>Custom element</p> <output aria-label="Element count">{ctx.count}</output> <div class="counter-controls"> <button type="button" aria-label="Decrement" onClick={() => ctx.decrement()} > − </button> <button type="button" aria-label="Increment" onClick={() => ctx.increment()} > + </button> </div> </section>));src/counter.css — styles inside the element
:host { display: block; color: inherit; font: inherit;}
.counter-card { display: grid; justify-items: center; gap: 0.75rem; padding: 1.25rem; border: 1px solid var(--sl-color-gray-4, GrayText); border-radius: 0.5rem; background: var(--sl-color-bg, Canvas);}
p { margin: 0;}
output { font-size: 2rem; font-weight: 600; font-variant-numeric: tabular-nums;}
.counter-controls { display: flex; gap: 0.5rem;}
button { min-width: 2.75rem; min-height: 2.75rem; border: 1px solid var(--sl-color-gray-4, GrayText); border-radius: 0.375rem; background: var(--sl-color-bg, Canvas); color: inherit; font: inherit; cursor: pointer;}
button:hover { background: var(--sl-color-bg-inline-code, ButtonFace);}
button:focus-visible { outline: 2px solid var(--sl-color-text-accent, Highlight); outline-offset: 3px;}The live demos run these same modules from the React example.
Plain HTML, Vue, and Svelte can use the registered tag, attributes, native DOM events, and element commands.
These browser capabilities do not imply a new Ignite Vue/Solid hook.
See the framework examples.
Styling
Section titled “Styling”Use native CSS inside the shadow root.
Return a <style> element containing this CSS alongside the view:
:host { display: block; color: var(--counter-color, currentColor);}button { font: inherit; padding: 0.5rem 1rem;}Global selectors do not pierce shadow roots; inherited custom properties support theming.
Account for your application’s CSP when using inline styles.
Use native controls, labels, and keyboard behavior; see accessibility.
Presentation resources
Section titled “Presentation resources”Retained Canvas, WebGL, editor, map, and native view resources belong to presentation lifecycle facilities.
Ignite effects are notifications, not resource or commit hooks.