Mutations
Mutations are the only permitted way to change client state. They are synchronous pure functions — network requests, DOM manipulation, and timers are structurally excluded. After every mutation, Pulse automatically applies constraints and re-renders the view.
What is a mutation?
A mutation is a function with the signature (state, event) => partialState. It receives the current state and the DOM event that triggered it, and returns a plain object to merge into state.
mutations: {
increment: (state) => ({ count: state.count + 1 }),
decrement: (state) => ({ count: state.count - 1 }),
reset: () => ({ count: 0 }),
}
The returned partial is shallow-merged. Only the returned keys are changed — everything else in state is preserved.
Binding mutations to DOM events
Mutations are bound to DOM events using the data-event attribute in the view HTML:
<"tok-fn">class="tok-kw">button "tok-fn">data-event="increment">+</"tok-fn">class="tok-kw">button> <!-- click → increment -->
<"tok-fn">class="tok-kw">button "tok-fn">data-event="click:decrement">-</"tok-fn">class="tok-kw">button> <!-- explicit click -->
<"tok-fn">class="tok-kw">input "tok-fn">data-event="change:setName"> <!-- change event -->
<"tok-fn">class="tok-kw">input "tok-fn">data-event="input:setQuery"> <!-- input event (every keystroke) -->
| Event type | Shorthand | Typical use |
|---|---|---|
click | data-event="mutName" | Buttons, links |
change | data-event="change:mutName" | Select dropdowns, checkboxes |
input | data-event="input:mutName" | Search/filter fields — add data-debounce="300" to rate-limit |
Debounce and throttle
Add data-debounce="300" alongside data-event to delay the mutation until typing stops. The mutation fires once, 300ms after the last keystroke — not on every character. Use this for live search and filter inputs.
<"tok-fn">class="tok-kw">input "tok-fn">data-event="input:search" "tok-fn">data-debounce="300">
<"tok-fn">class="tok-kw">input "tok-fn">data-event="input:filter" "tok-fn">data-throttle="100">
data-throttle="100" fires at most once per 100ms — useful when you want frequent updates but need to limit the rate. Both attributes accept a value in milliseconds and apply to input and change events. No per-spec timer code needed.
The event argument
The second argument to a mutation is the native DOM Event object, giving access to the element and its value:
mutations: {
setName: (state, e) => ({ name: e.target.value }),
setCountry: (state, e) => ({ country: e.target.value }),
toggle: (state, e) => ({ checked: e.target.checked }),
}
Partial state merge
Only the keys that need to change are returned. The runtime merges the returned object into the existing state at the top level:
// state = { step: 2, name: 'Alice', email: 'a@b.com' }
mutations: {
nextStep: (state) => ({ step: state.step + 1 }),
// After: { step: 3, name: 'Alice', email: 'a@b.com' }
// name and email are untouched
}
mutations: {
setEmail: (state, e) => ({
// Spread the nested object to preserve sibling keys
user: { ...state.user, email: e.target.value }
}),
}
No side effects
Mutations are pure functions. Network requests, DOM manipulation, and timers belong in actions — not here. The structural separation is what lets Pulse re-render predictably, apply constraints safely, and make state changes auditable.
Mutations and constraints
After every mutation, Pulse automatically applies any constraints declared in the spec. State bounds are never checked inside mutations — they are declared once and enforced by the framework regardless of what a mutation returns:
{
state: { count: 0 },
constraints: { count: { min: 0, max: 10 } },
mutations: {
increment: (state) => ({ count: state.count + 1 }),
// count is automatically clamped to 10 — no need to check here
}
}
Mutations and forms
Mirroring every keystroke into state via data-event="input:..." causes innerHTML replacement on each keypress, which destroys input focus. Pulse prevents this by keeping form inputs uncontrolled — values are captured via FormData in an action's onStart, before the view re-renders:
<!-- mirroring every keystroke causes focus loss -->
<"tok-fn">class="tok-kw">input "tok-fn">data-event="input:setEmail">
<!-- uncontrolled: values captured at submit via FormData -->
<"tok-fn">class="tok-kw">form "tok-fn">data-action="submit">
<"tok-fn">class="tok-kw">input "tok-fn">name="email" "tok-fn">type="email">
<"tok-fn">class="tok-kw">button>Submit</"tok-fn">class="tok-kw">button>
</"tok-fn">class="tok-kw">form>