# Sygnal > Reactive component framework (Cycle.js Model-View-Intent, JSX views, xstream streams). `npm i sygnal`; Vite: `import sygnal from 'sygnal/vite'`, `plugins: [sygnal()]`; entry: `run(App)`. This file shows only the canonical forms. Strict mode (`sygnal-check --strict`, runtime `strict: true`) flags other forms as SYG5xx; forms 6.0 removed (positional views, `'A | SINK'` keys, `CHILD.select('Name')`, `.components`, `.peers`, `component()`) are SYG612: https://sygnal.js.org/guide/migrating-to-6/ ## 1. Mental model - A component is a pure view function plus static properties: `.intent` (WHEN: sources → named action streams), `.model` (WHAT: action → one reducer per sink), `.initialState`. - There is one state tree. A child sees its parent's whole state, or a slice with `state="key"`. A Collection item sees one array element. - Every side effect is a sink of a model entry (STATE, EVENTS, PARENT, EFFECT, custom drivers). JSX never has event handlers (`onClick`). - Streams are xstream, not RxJS. An intent's DOM selectors only see the component's own JSX, never a child component's. ## 2. Component anatomy ```jsx import { ABORT } from 'sygnal' function Counter({ state, context, label }) { // 1st arg: parent props + state, context, children, slots return (
{label} {state.count} (x2 = {state.double})
) } Counter.initialState = { count: 0 } // root only: a child gets state from its parent (SYG405) Counter.calculated = { double: state => state.count * 2 } // or [['count'], fn]; read as state.double Counter.context = { total: state => state.count } // itself + descendants: ({ context }) => context.total Counter.intent = ({ DOM }) => ({ // sources: DOM STATE EVENTS CHILD props$ commands$ dispose$ + drivers INC: DOM.click('.inc'), RESET: DOM.click('.reset'), }) Counter.model = { INC: (state, data, next, props) => ({ ...state, count: state.count + 1 }), RESET: (state) => (state.count === 0 ? ABORT : { ...state, count: 0 }), } Counter.onError = (error, { componentName }) =>
{componentName} failed
export default Counter ``` - Reducer `(state, data, next, props)`. `data` is the action stream's value. `next('ACTION', data?, delayMs = 10)` dispatches another action of this component `delayMs` after the call, whenever it is called: also later, from a timer or callback in an EFFECT (HTTP goes through `makeFetchDriver`, not `fetch` + `next()`). `props` holds the parent's props plus `state`, `context`, `children`, `slots`. - Model entry: a function is the STATE reducer. An object `{ STATE, EVENTS, PARENT, EFFECT, LOG, : fn }` maps each sink to a `(state, data, next, props)` function whose return value goes to that sink. `: true` forwards `data` as-is. - **Every sink of one entry sees the state from before this action**: EVENTS, PARENT, EFFECT and drivers never see what STATE returns. Compute the new value from `(state, data)` inside the sink: `INC: { STATE: s => ({ ...s, n: s.n + 1 }), PARENT: s => ({ n: s.n + 1 }) }`. - Built-in actions (model only): `BOOTSTRAP` (once, just after mount), `INITIALIZE` (automatic: sets initialState), `DISPOSE` (unmount). `HYDRATE` is not built in (6.0): SSR data comes from Vike `+data` / `hydrateState`. Declaration statics (`connections`, `resources`, `route`, `head`) are computed from state, with or without a model; a root that declares one needs `initialState` (SYG132). Also reserved as statics: `timers`, `uses`, `persist`, `viewTransitions` (§3). ## 3. Canonical examples ### State update, ABORT, set / toggle, controlled input ```jsx import { ABORT, set, toggle } from 'sygnal' function AddTodo({ state }) { return (
{state.showHelp &&

Type, then Add

}
) } AddTodo.initialState = { draft: '', todos: [], nextId: 1, showHelp: false } AddTodo.intent = ({ DOM }) => ({ DRAFT: DOM.input('.draft').value(), // a bound value={...} needs an input listener (SYG111) ADD: DOM.click('.add'), HELP: DOM.click('.help'), }) AddTodo.model = { DRAFT: set((state, draft) => ({ draft })), // merges: ({ ...state, draft }); never set('draft') (SYG221) ADD: (state) => { const text = state.draft.trim() if (!text) return ABORT // "no change" (returning the same `state` works too) return { ...state, draft: '', nextId: state.nextId + 1, todos: [...state.todos, { id: state.nextId, text }] } }, HELP: toggle('showHelp'), } ``` ### Multi-sink entry + EVENTS between non-adjacent components ```jsx import { event } from 'sygnal' function SaveButton({ state }) { return } SaveButton.intent = ({ DOM }) => ({ SAVE: DOM.click('.save') }) SaveButton.model = { SAVE: { STATE: (state) => ({ ...state, saving: true }), EVENTS: event('DOC_SAVED', (state) => ({ id: state.id })), // or event('RESET'), event('MODE', 'dark') }, } function Toast({ state }) { // anywhere in the tree: EVENTS is a global bus return

{state.lastSaved}

} Toast.intent = ({ EVENTS }) => ({ SAVED: EVENTS.select('DOC_SAVED') }) // emits the payload only Toast.model = { SAVED: (state, payload) => ({ ...state, lastSaved: payload.id }) } ``` ### Child → parent (PARENT + CHILD.select), Collection, item removal ```jsx import { Collection } from 'sygnal' function TaskItem({ state }) { return (
  • {state.title}
  • ) } TaskItem.intent = ({ DOM }) => ({ PICK: DOM.click('.pick'), REMOVE: DOM.click('.remove') }) TaskItem.model = { PICK: { PARENT: (state) => ({ taskId: state.id }) }, // the parent receives exactly this value REMOVE: () => undefined, // a Collection item removes itself } function TaskList({ state }) { return (

    {state.picked}

    ) } TaskList.initialState = { picked: null, tasks: [{ id: 1, title: 'Write' }, { id: 2, title: 'Test' }] } TaskList.intent = ({ CHILD }) => ({ PICKED: CHILD.select(TaskItem) }) // the function, not a string TaskList.model = { PICKED: (state, { taskId }) => ({ ...state, picked: taskId }) } ``` - `from` names an array field. Items are keyed by `.id` (by index if they have none). `filter={t => !t.done}`; `sort="title"`, `{ title: 'desc' }`, an array of those, or a compare function. Sorting and filtering only change what renders: the state array keeps its order, and an item's edit (`{ ...state, done: true }`) is written back to its element by key. Items render directly into the parent element, with no wrapper (`` is SYG612: put the class on your own `