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 busEvent objects { type, data }
CHILD.select(ComponentFn) — Events from child components
PARENTValues sent to the parent component
READYBoolean signal for Suspense boundaries
LOGAny 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():

import { run } from 'sygnal'
import RootComponent from './RootComponent.jsx'
import myCustomDriver from './drivers/myCustomDriver'
run(RootComponent, {
CUSTOM: myCustomDriver
})

The driver is then available as both a source (in intent) and a sink (in model):

MyComponent.intent = ({ DOM, CUSTOM }) => ({
DATA_RECEIVED: CUSTOM.select('some-category')
})
MyComponent.model = {
FETCH: {
CUSTOM: (state) => ({ category: 'some-category', url: '/api/data' })
}
}

The EVENTS driver provides a lightweight pub/sub system for cross-component communication:

// Publishing events (in model)
Publisher.model = {
NOTIFY: {
EVENTS: (state) => ({ type: 'notification', data: { message: 'Hello!' } })
}
}
// Subscribing to events (in intent)
Subscriber.intent = ({ EVENTS }) => ({
HANDLE_NOTIFICATION: EVENTS.select('notification')
})

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}`
}
}

A Cycle.js driver is a function that takes a sink stream and returns a source object:

function myDriver(sink$) {
// Listen to commands from the app
sink$.addListener({
next: (command) => {
// Perform side effects here
console.log('Received command:', command)
}
})
// Return a source for the app to observe
return {
select: (type) => {
// Return a filtered stream
}
}
}

For the common case of wrapping a Promise-returning function as a driver, use driverFromAsync():

import { driverFromAsync } from 'sygnal'
const apiDriver = driverFromAsync(
async (url) => {
const response = await fetch(url)
return response.json()
},
{
selector: 'endpoint', // Property name for categorizing requests
args: 'url', // Property to extract as function arguments
return: 'data', // Property name for the return value
pre: (incoming) => incoming, // Pre-process incoming commands
post: (result, incoming) => result // Post-process results
}
)
// Register the driver
run(RootComponent, { API: apiDriver })
OptionTypeDefaultDescription
selectorString'category'Property used to categorize/filter responses
argsString, Array, or Function'value'How to extract function arguments from incoming commands
returnString'value'Property name to wrap the return value in
preFunctionIdentityPre-process incoming sink values
postFunctionIdentityPost-process results before sending to source
// Intent — receive API responses
MyComponent.intent = ({ API }) => ({
DATA_LOADED: API.select('users') // Filter by the selector property
})
// Model — send API requests
MyComponent.model = {
FETCH_USERS: {
API: (state) => ({ endpoint: 'users', url: '/api/users' })
},
DATA_LOADED: (state, data) => ({ ...state, users: data.data })
}