Getting Started
•5 min read
Choosing the Right Primitive
When to use each Directive primitive — and when not to.
Decision Tree
Start here when you're unsure which primitive to reach for:
- Are you storing a value the user or server provides directly? → Fact
- Are you computing a value from other facts? → Derivation
- Do you need to react to a user action synchronously? → Event
- Do you need to declare "this must be true" and let the runtime figure out how? → Constraint + Resolver
- Do you need a fire-and-forget side effect (logging, DOM updates, analytics)? → Effect
If you're still unsure, ask: does this thing produce requirements, or consume them? Constraints produce requirements. Resolvers consume them. Everything else is either data (facts, derivations) or reactions (events, effects).
Comparison Table
| Primitive | Purpose | Sync/Async | Reads State | Writes State | Example |
|---|---|---|---|---|---|
| Fact | Source of truth | Sync | — | Yes | facts.user = data |
| Derivation | Computed value | Sync | Yes | No | isAdmin: (facts) => facts.role === 'admin' |
| Event | Synchronous mutation | Sync | Yes | Yes | events.addItem(facts, payload) |
| Constraint | Declares a requirement | Sync or Async | Yes | No | when: (facts) => !facts.user → require: { type: 'FETCH_USER' } |
| Resolver | Fulfills a requirement | Async | Yes | Yes | resolve: async (req, context) => { ... } |
| Effect | Side effect | Sync | Yes | No (should not) | run: (facts) => document.title = facts.title |
Key distinctions:
- Derivations vs Effects: Derivations compute values. Effects perform side effects. If you need the result, it's a derivation. If you need the action, it's an effect.
- Events vs Resolvers: Events are synchronous and immediate. Resolvers handle async work triggered by constraints.
- Constraints vs Effects: Constraints say "something is missing." Effects say "something changed, react to it."
Common Mistakes
Putting async logic in events
// Wrong — events are synchronous
events: {
fetchUser: async (facts) => {
const res = await fetch('/api/user'); // Don't do this
facts.user = await res.json();
},
},
// Right — use a constraint + resolver
constraints: {
needsUser: {
when: (facts) => !facts.user && facts.token,
require: { type: 'FETCH_USER' },
},
},
resolvers: {
fetchUser: {
requirement: 'FETCH_USER',
resolve: async (req, context) => {
const res = await fetch('/api/user');
context.facts.user = await res.json();
},
},
},
Mutating facts in effects
// Wrong — effects shouldn't write to facts
effects: {
syncTheme: {
run: (facts) => {
facts.theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' : 'light'; // Don't mutate facts here
},
},
},
// Right — use a constraint + resolver to read the system preference
constraints: {
needsThemeDetection: {
when: (facts) => !facts.themeDetected,
require: { type: 'DETECT_THEME' },
},
},
resolvers: {
detectTheme: {
requirement: 'DETECT_THEME',
resolve: async (req, context) => {
const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
context.facts.theme = dark ? 'dark' : 'light';
context.facts.themeDetected = true;
},
},
},
Using constraints for synchronous transforms
// Wrong — constraint + resolver for a simple computation
constraints: {
needsFullName: {
when: (facts) => !facts.fullName && facts.firstName,
require: { type: 'COMPUTE_NAME' },
},
},
// Right — just use a derivation
derive: {
fullName: (facts) => `${facts.firstName} ${facts.lastName}`,
},
Over-constraining: when a simple event is enough
// Overkill — constraint + resolver for a synchronous toggle
constraints: {
needsToggle: {
when: (facts) => facts.toggleRequested,
require: { type: 'TOGGLE_SIDEBAR' },
},
},
// Right — just use an event
events: {
toggleSidebar: (facts) => {
facts.sidebarOpen = !facts.sidebarOpen;
},
},
Same Feature, Two Ways
1. Theme Toggle
Wrong: Constraint + Resolver
constraints: {
needsThemeSwitch: {
when: (facts) => facts.themeChangeRequested,
require: { type: 'SWITCH_THEME' },
},
},
resolvers: {
switchTheme: {
requirement: 'SWITCH_THEME',
resolve: async (req, context) => {
context.facts.theme = context.facts.theme === 'light' ? 'dark' : 'light';
context.facts.themeChangeRequested = false;
},
},
},
Right: Event (synchronous, no async needed)
events: {
toggleTheme: (facts) => {
facts.theme = facts.theme === 'light' ? 'dark' : 'light';
},
},
2. Filtered List
Wrong: Effect that writes facts
effects: {
filterItems: {
deps: ['searchQuery', 'items'],
run: (facts) => {
facts.filtered = facts.items.filter(i => i.name.includes(facts.searchQuery));
},
},
},
Right: Derivation (pure computation)
derive: {
filteredItems: (facts) => {
return facts.items.filter(i => i.name.includes(facts.searchQuery));
},
},
3. Loading User Data
Wrong: Event with async logic
events: {
loadUser: async (facts) => {
facts.loading = true;
const user = await fetchUser(facts.userId);
facts.user = user;
facts.loading = false;
},
},
Right: Constraint + Resolver
constraints: {
needsUser: {
when: (facts) => !facts.user && facts.userId,
require: (facts) => ({ type: 'LOAD_USER', userId: facts.userId }),
},
},
resolvers: {
loadUser: {
requirement: 'LOAD_USER',
resolve: async (req, context) => {
const user = await fetchUser(req.userId);
context.facts.user = user;
},
},
},
4. Page Title Sync
Wrong: Derivation with side effects
derive: {
pageTitle: (facts) => {
const title = `${facts.currentPage} - MyApp`;
document.title = title; // Side effect in a derivation!
return title;
},
},
Right: Derivation + Effect
derive: {
pageTitle: (facts) => `${facts.currentPage} - MyApp`,
},
effects: {
syncTitle: {
deps: ["currentPage"],
run: (facts) => {
document.title = `${facts.currentPage} - MyApp`;
},
},
},
5. Auto-Save
Wrong: Effect that triggers async work
effects: {
autoSave: {
deps: ['document'],
run: async (facts) => {
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify(facts.document),
});
},
},
},
Right: Constraint + Resolver (with debounce)
constraints: {
needsSave: {
when: (facts) => facts.isDirty && !facts.isSaving,
require: (facts) => ({ type: 'SAVE_DOCUMENT', content: facts.document }),
},
},
resolvers: {
saveDocument: {
requirement: 'SAVE_DOCUMENT',
resolve: async (req, context) => {
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify(req.content),
});
context.facts.isDirty = false;
},
},
},
Related
- Core Concepts — overview of all primitives
- Facts — proxy-based state
- Derivations — auto-tracked computed values
- Constraints — declaring requirements
- Resolvers — fulfilling requirements
- Effects — fire-and-forget side effects
- Events — synchronous mutations

