Guides
•3 min read
How to Use batch() to Prevent Glitches
Multi-field updates that never expose intermediate states to constraints or the UI.
The Problem
When you update multiple related facts one at a time, constraints evaluate between each update. If a constraint reads both status and items, updating status first triggers evaluation with the new status but stale items – a "glitch." The constraint may emit requirements based on this inconsistent intermediate state, causing unnecessary API calls or incorrect UI.
The Solution
import { createModule, t } from '@directive-run/core';
const checkout = createModule('checkout', {
schema: {
items: t.array<{ id: string; price: number }>(),
status: t.string<'idle' | 'processing' | 'complete' | 'error'>(),
total: t.number(),
orderId: t.string().optional(),
},
init: (facts) => {
facts.items = [];
facts.status = 'idle';
facts.total = 0;
facts.orderId = undefined;
},
constraints: {
needsPayment: {
when: (facts) => facts.status === 'processing' && facts.total > 0,
require: (facts) => ({
type: 'PROCESS_PAYMENT',
total: facts.total,
items: facts.items,
}),
},
},
resolvers: {
processPayment: {
requirement: 'PROCESS_PAYMENT',
resolve: async (req, context) => {
const res = await fetch('/api/payment', {
method: 'POST',
body: JSON.stringify({ total: req.total, items: req.items }),
});
const data = await res.json();
// Batch the completion update
context.system.batch(() => {
context.facts.status = 'complete';
context.facts.orderId = data.orderId;
context.facts.items = [];
context.facts.total = 0;
});
},
},
},
});
function CheckoutButton({ system }) {
const { facts } = useDirective(system);
const handleCheckout = () => {
// Without batch: constraints see status='processing' with old total
// With batch: constraints see both updates atomically
system.batch(() => {
system.facts.status = 'processing';
system.facts.total = system.facts.items.reduce(
(sum, i) => sum + i.price, 0,
);
});
};
return (
<button onClick={handleCheckout} disabled={facts.status === 'processing'}>
Checkout ({facts.items.length} items)
</button>
);
}
Step by Step
system.batch()defers all notifications – fact changes inside the callback are applied immediately to the store, but listeners (constraints, derivations, effects, React subscribers) are not notified until the batch completes.Constraints see consistent state – when
needsPaymentevaluates after the batch, bothstatusandtotalreflect the new values. There's no intermediate state where status is'processing'but total is still0.Batches inside resolvers use
context.system.batch()– when a resolver completes and updates multiple facts, batching prevents the UI from briefly showing a "complete" status with stale items.Derivations recompute once – without batch, changing
itemsthentotaltriggers two derivation cycles. With batch, derivations recompute once with the final state.
Common Variations
Resetting a form
function resetForm(system) {
system.batch(() => {
system.facts.name = '';
system.facts.email = '';
system.facts.phone = '';
system.facts.errors = {};
system.facts.touched = {};
system.facts.submitStatus = 'idle';
});
}
Batch with return value
// batch() returns whatever the callback returns
const total = system.batch(() => {
system.facts.items = newItems;
const sum = newItems.reduce((s, i) => s + i.price, 0);
system.facts.total = sum;
return sum;
});
console.log(total); // The computed sum
Nested batches
// Nested batches are safe – only the outermost batch flushes
system.batch(() => {
system.facts.status = 'loading';
system.batch(() => {
// This inner batch doesn't trigger an intermediate flush
system.facts.items = [];
system.facts.total = 0;
});
system.facts.message = 'Resetting...';
});
// All four changes flush together here
Related
- Facts – store internals and notification model
- Constraints – evaluation timing
- WebSocket Connections – batching socket events
- Optimize Re-Renders – reducing React updates

