State data already has a precise owner and lifetime. .update changes part of one active state by
merging a Schema-shaped patch, while every field omitted from that patch stays intact.
Change state-owned data without replacing the state.
.update merges a partial record into one active state. Its computation can use the current event and the results of earlier actions in the same ordered fold.
1 · Handle without changing data
The event is handled, but the message stays put.
Sign accepts ChangeMessage with a targetless handler. Sending a decoded message returns Handled, yet the state-owned fields remain unchanged because the edge declares no update action.
import { State, Statechart, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const ChangeMessage = Schema.TaggedStruct("ChangeMessage", {
message: Schema.String,
});
export class Sign extends State.Atomic<Sign>()("Sign", {
message: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("Welcome")),
),
revision: Schema.Finite.pipe(
Schema.withConstructorDefault(Effect.succeed(0)),
),
foldedMessage: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("")),
),
}) {
static transitions = Transitions.make(this, ({ on }) => [on(ChangeMessage)]);
}
export const SignChart = Statechart.make(Sign);
2 · Merge an event-derived patch
.update changes the field its target owns.
The same handler now updates Sign with a partial record derived from event.message. Sign remains active, its message changes, and the untouched revision fields retain their values.
import { State, Statechart, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const ChangeMessage = Schema.TaggedStruct("ChangeMessage", {
message: Schema.String,
});
export class Sign extends State.Atomic<Sign>()("Sign", {
message: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("Welcome")),
),
revision: Schema.Finite.pipe(
Schema.withConstructorDefault(Effect.succeed(0)),
),
foldedMessage: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("")),
),
}) {
static transitions = Transitions.make(this, ({ on }) => [
on(ChangeMessage).update(Sign, ({ event }) => ({
message: event.message,
})),
]);
}
export const SignChart = Statechart.make(Sign);
3 · Fold updates in order
A later update reads the result folded so far.
A second update reads Sign after the message patch. It records the new message and increments revision, proving that chained updates run in authored order rather than against one stale starting value.
import * as Effect from "effect/Effect";
import { Schema } from "effect";
import { Query, State, Statechart, Transitions } from "@motive/motive";
const ChangeMessage = Schema.TaggedStruct("ChangeMessage", {
message: Schema.String,
});
const recordFold = Query.gen(function* () {
const sign = yield* Sign;
return {
foldedMessage: sign.message,
revision: sign.revision + 1,
};
});
export class Sign extends State.Atomic<Sign>()("Sign", {
message: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("Welcome")),
),
revision: Schema.Finite.pipe(
Schema.withConstructorDefault(Effect.succeed(0)),
),
foldedMessage: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("")),
),
}) {
static transitions = Transitions.make(this, ({ on }) => [
on(ChangeMessage)
.update(Sign, ({ event }) => ({ message: event.message }))
.update(Sign, recordFold),
]);
}
export const SignChart = Statechart.make(Sign);
Handling is not mutation
The first frame gives Sign a targetless ChangeMessage handler. Send the event and the client
returns Handled, but message, revision, and foldedMessage do not change.
An event does not mutate chart data merely because its object contains a similarly named field. The model must author the data action explicitly.
An update names its owner
The second frame chains an update to the same handler:
on(ChangeMessage).update(Sign, ({ event }) => ({
message: event.message,
}));
Sign is both the update target and the Schema that defines the accepted patch fields. Returning an
undeclared property is a type error. Omitting revision and foldedMessage preserves their current
values rather than replacing the whole facet.
Update only active state data
An update target must be active when the action runs. That is what makes its data available and its lifetime meaningful. Updating an inactive sibling is a defect, not an implicit way to activate or preload that state.
Place longer-lived data on a longer-lived owner when several child states need to update it. The topology should express that shared lifetime instead of weakening the active-state rule.
The computation may read the event
The inline callback receives the decoded ChangeMessage event selected by this edge. Its
event.message is already a string because the event Schema admitted it before transition
selection.
For more involved patches, pass a Query instead. Its strict and optional reads make every state
and event dependency explicit in the same way as the Queries category.
Chained updates form an ordered fold
The final frame keeps the message patch first and adds recordFold second. That query reads the
current Sign value, copies sign.message into foldedMessage, and increments revision.
Send Platform 4. Both displayed message fields become Platform 4, proving that the second update
read the result of the first—not the old Welcome value. Reverse the action order and the folded
message records the old value instead.
One update does not require a transition target
All three handlers are targetless. .update changes data; the missing transition target preserves
the active configuration. A targeted transition may carry updates too, but movement and mutation
remain separate authored choices.
Entry and exit branches use the same action link. Their lifecycle boundary decides when the update
runs; .update still decides which active state owns the patch.
Test the fold
- Send different messages in all three frames and compare handled events with actual data changes.
- Reverse the two updates in the final frame and predict
foldedMessagebefore running it. - Add a second field to
ChangeMessageand update two declaredSignfields in one patch.