Run the workflow model by hand.
These demos mirror the core contracts: explicit state, renderings with callbacks, parent-child output routing, and render-scoped workers keyed by logical effect identity.
Simple counter workflow.
The UI only receives the current rendering. Each button sends a pure action into the runtime, producing a new state and sometimes an output.
const counterWorkflow = {
initialState: () => ({ count: 0 }),
render: (_props, state, ctx) => {
const increment = () => ctx.actionSink.send((s) => ({
state: { count: Math.min(s.count + 1, 10) },
output: s.count + 1 === 10 ? { type: 'reachedMax', value: 10 } : undefined,
}));
const decrement = () => ctx.actionSink.send((s) => ({
state: { count: Math.max(s.count - 1, 0) },
output: s.count - 1 === 0 ? { type: 'reachedZero' } : undefined,
}));
const reset = () => ctx.actionSink.send(() => ({ state: { count: 0 } }));
if (state.count === 0) return { type: 'atZero', count: 0, increment };
if (state.count === 10) return { type: 'atMax', count: 10, reset };
return { type: 'counting', count: state.count, increment, decrement, reset };
},
};
import { useWorkflow } from '@workflow-ts/react';
import { counterWorkflow } from './counterWorkflow';
export function CounterScreen() {
const rendering = useWorkflow(counterWorkflow, {});
switch (rendering.type) {
case 'atZero':
return (
<div>
<span>{rendering.count}</span>
<button onClick={rendering.increment}>+</button>
</div>
);
case 'atMax':
return (
<div>
<span>{rendering.count}</span>
<button onClick={rendering.reset}>Reset</button>
</div>
);
case 'counting':
return (
<div>
<button onClick={rendering.decrement}>−</button>
<span>{rendering.count}</span>
<button onClick={rendering.increment}>+</button>
<button onClick={rendering.reset}>Reset</button>
</div>
);
}
}
Compose parent and child workflows.
A parent renders a child with a stable key, receives child outputs, and maps them into parent actions. The child keeps its own state while the parent owns the wider flow.
CartWorkflow
state: editingCart
QuantityCounter
render: (props, state, ctx) => {
const quantity = ctx.renderChild(
counterWorkflow,
{ max: 5 },
'quantity',
(childOutput) => (parentState) => ({
state:
childOutput.type === 'reachedMax'
? { ...parentState, banner: 'Bulk quantity selected' }
: { ...parentState, banner: 'Quantity reset to zero' },
}),
);
return { type: 'editingCart', quantity };
};
const counterWorkflow = {
initialState: () => ({ count: 0 }),
render: (_props, state, ctx) => {
const increment = () => ctx.actionSink.send((s) => ({
state: { count: Math.min(s.count + 1, 5) },
output: s.count + 1 === 5 ? { type: 'reachedMax', value: 5 } : undefined,
}));
const decrement = () => ctx.actionSink.send((s) => ({
state: { count: Math.max(s.count - 1, 0) },
output: s.count - 1 === 0 ? { type: 'reachedZero' } : undefined,
}));
const reset = () => ctx.actionSink.send(() => ({ state: { count: 0 } }));
return { type: 'counting', count: state.count, increment, decrement, reset };
},
};
import { useWorkflow } from '@workflow-ts/react';
import { cartWorkflow } from './cartWorkflow';
export function CartScreen() {
const rendering = useWorkflow(cartWorkflow, {});
return (
<section>
<h2>Cart</h2>
{rendering.banner && <p className="banner">{rendering.banner}</p>}
<div className="quantity">
<button onClick={rendering.quantity.decrement}>−</button>
<span>{rendering.quantity.count}</span>
<button onClick={rendering.quantity.increment}>+</button>
</div>
<button onClick={rendering.quantity.reset}>Reset</button>
</section>
);
}
Worker keys control lifetime.
Start a worker in loading. Move to a state that also declares
runWorker(..., 'load') and the same worker keeps running. Move to a state
that does not declare that key and the runtime cancels it at the end of render.
Press Start load to render loading and run the worker.
- worker key
- load
- rendering
- idle
const rendering = useWorkflow(loadWorkflow, undefined);
if (rendering.type === 'loading') {
return (
<>
<button onClick={rendering.next}>Next state</button>
</>
);
}
if (rendering.type === 'loaded') {
return <p>Loaded.</p>;
}