Skip to content

Drivers

Drivers handle all side effects in a Sygnal application. They are the bridge between your pure component code and the outside world.

Every driver has two sides:

  • Source — Provides data to your component (e.g., DOM events, API responses)
  • Sink — Receives commands from your component (e.g., state updates, log messages)

Sygnal’s run() function automatically includes these drivers:

Driver / SourceSource (in intent)Sink (in model)
DOM.select(css).events(event) or shorthand .click(css)Handled automatically by the view
STATE.stream — The state ObservableReducer functions from model
EVENTS.select(type) — Custom event busEvents, built with event('TYPE', payload)
CHILD.select(ComponentFn) — Events from child components—
PARENT—Values sent to the parent component
EFFECT—Side effects only; sends nothing (see Effect Handlers)
READY—Boolean signal for Suspense boundaries
LOG—Any value — logged to the console
props$Stream of props passed from the parent—
children$Stream of children passed from the parent—
context$Stream of merged context values from ancestors—
dispose$Emits true once on unmount—

Pass additional drivers as the second argument to run(). Each one is then a source (in intent) and a sink (in model) under that name:

import { run, makeFetchDriver, makeSocketDriver } from 'sygnal'
import App from './App.jsx'
run(App, {
HTTP: makeFetchDriver(),
WS: makeSocketDriver(),
})

Drivers are registered explicitly, so an app only ships the ones it uses. A sink or source with no driver is reported as SYG609 (with the dev diagnostics on). In tests, renderComponent needs no drivers: a driver you don’t pass is replaced by a fake you answer from the test (Testing).

DriverForGuide
makeFetchDriver()HTTP requests; replies arrive as the actions the request namesHTTP
makeSocketDriver()WebSocket and server-sent events, declared with the connections staticSockets
makeChatDriver({ transport }) (sygnal/ai)Streaming LLM chat; the reply arrives as delta, ok and error actionsAI Chat
driverFromAsync(fn)Any other promise-returning functionCustom Drivers
your ownAnything else: a function sink$ => sourceCustom Drivers

A model entry sends a request and names the actions that receive the reply; the intent has no line for it:

Quote.model = {
LOAD: {
STATE: (state) => ({ ...state, status: 'loading' }),
HTTP: (state, id) => ({ url: `/api/quotes/${id}`, ok: 'LOADED', error: 'FAILED', latest: true }),
},
LOADED: (state, quote) => ({ ...state, status: 'done', quote }), // the parsed body
FAILED: (state, { status, error }) => ({ ...state, status: status === 404 ? 'missing' : 'error' }),
}

The full guide (latest-only requests, aborting, the request fields, testing) is on the HTTP page.

A component declares its connections as a function of its state, and their events arrive as the actions it names:

Chat.connections = (state) => ({
room: state.room && { socket: `/ws/rooms/${state.room}`, message: 'RECEIVED', open: 'CONNECTED', close: 'DROPPED' },
})
Chat.model = {
SAY: { WS: (state, text) => ({ to: 'room', json: { text } }) },
RECEIVED: (state, msg) => ({ ...state, messages: [...state.messages, msg] }),
CONNECTED: (state) => ({ ...state, online: true }),
DROPPED: (state) => ({ ...state, online: false }),
}

See Sockets.

The EVENTS driver provides a lightweight pub/sub system for communication between components that aren’t parent and child:

import { event } from 'sygnal'
// Publishing events (in model)
Publisher.model = {
NOTIFY: {
EVENTS: event('NOTIFICATION', (state) => ({ message: state.message })),
},
}
// Subscribing to events (in intent): the stream emits the event's data
Subscriber.intent = ({ EVENTS }) => ({
HANDLE_NOTIFICATION: EVENTS.select('NOTIFICATION'),
})

The bus is global (it isn’t isolated per component) and a broadcast: every component that selects a type receives it, and an event nobody selects is dropped. A type that is emitted but never selected, or selected but never emitted, is reported as SYG105, which usually means a typo. With TypeScript, the SygnalEvents registry checks names and payloads.

The LOG driver sends values to the browser console:

MyComponent.model = {
SOME_ACTION: {
STATE: (state) => ({ ...state, updated: true }),
LOG: (state, data) => `Action triggered with: ${data}`
}
}