Skip to content

Model

The .model property defines what happens for each action. It maps action names to reducers or driver commands.

The most common case is updating state. A function provided directly as an action value is treated as a state reducer:

MyComponent.model = {
INCREMENT: (state) => ({ ...state, count: state.count + 1 }),
SET_NAME: (state, data) => ({ ...state, name: data })
}

Reducer arguments:

  1. state — The current component state
  2. data — The value emitted by the triggering stream in intent
  3. next — A function to trigger other actions (see Chaining Actions)
  4. extra — An object containing { context, props, children }

When an action needs to do more than just update state, use an object to specify multiple driver sinks:

MyComponent.model = {
SAVE: {
STATE: (state, data) => ({ ...state, saved: true }),
LOG: (state, data) => `Saved: ${JSON.stringify(data)}`,
EVENTS: (state, data) => ({ type: 'saved', data })
}
}

Setting a driver sink to true passes the intent data through as-is:

MyComponent.model = {
LOG_DATA: {
LOG: true // passes whatever data came from intent directly to LOG
}
}

The next() function (third argument to any reducer) lets you trigger other actions:

MyComponent.model = {
SUBMIT: (state, data, next) => {
// Trigger VALIDATE after this action completes
next('VALIDATE', data)
return { ...state, submitting: true }
},
// next() with a delay (in milliseconds)
START: (state, data, next) => {
next('DELAYED_ACTION', null, 1000) // fires after 1 second
return state
},
VALIDATE: (state, data) => {
// handle validation
return { ...state, valid: true }
}
}

You can call next() multiple times in a single reducer, and optionally add a delay as the third argument.

Import ABORT from Sygnal and return it to cancel a state update:

import { ABORT } from 'sygnal'
MyComponent.model = {
MOVE: (state, data) => {
if (state.locked) return ABORT // skip this action entirely
return { ...state, position: data }
}
}

Sygnal provides four built-in actions that fire automatically:

ActionWhen It Fires
BOOTSTRAPOnce when the component is first instantiated (similar to React’s useEffect(() => {}, []))
INITIALIZEWhen the component receives its first state
HYDRATEWhen the component receives its first state during HMR
DISPOSEWhen the component is about to unmount — see Disposal Hooks
MyComponent.model = {
BOOTSTRAP: {
LOG: () => 'Component mounted!',
STATE: (state, data, next) => {
next('LOAD_DATA')
return state
}
},
INITIALIZE: (state) => {
// runs once when state is first available
return state
},
DISPOSE: {
EFFECT: () => console.log('Component unmounting'),
}
}