# Common Patterns Patterns for building CommonTools applications, organized by complexity. ## Core Principles ### 1. Use computed() for Data Transformations Reactive references in pattern bodies need `computed()` for transformations - direct iteration or operations will fail. Wrap transformations in `computed()`: ```typescript // ❌ WRONG const grouped = {}; for (const entry of entries) { // Error: reactive reference needs computed() grouped[entry.date] = entry; } // ✅ CORRECT const grouped = computed(() => { const result = {}; for (const entry of entries) { result[entry.date] = entry; } return result; }); ``` ### 2. Only Declare Cell<> When You Need to Mutate Everything is reactive by default. `Cell<>` in type signatures indicates you'll call `.set()`, `.push()`, or `.update()`: ```typescript interface Input { count: number; // Read-only (still reactive!) items: Cell; // Will mutate (call .push(), .set()) } ``` ### 3. Prefer Bidirectional Binding Over Handlers Before writing a handler, ask: "Am I just syncing UI ↔ data?" ```typescript // ✅ SIMPLE - No handler needed // Use handlers only for side effects, validation, or structural changes ``` --- ## Levels: Progressive Examples The following examples are complete, self-contained patterns illustrating progressive complexity. For real working patterns, see `packages/patterns/`. ## Level 1: Basic List The simplest pattern: a list with bidirectional binding and inline handlers. ```typescript import { Cell, Default, NAME, pattern, UI } from "commontools"; interface Item { title: string; done: Default; } interface Input { items: Cell; } export default pattern(({ items }) => ({ [NAME]: "Shopping List", [UI]: (
{items.map((item) => (
{item.title} { const current = items.get(); const index = current.findIndex((el) => Cell.equals(item, el)); if (index >= 0) items.set(current.toSpliced(index, 1)); }}>×
))} { const text = e.detail?.message?.trim(); if (text) items.push({ title: text, done: false }); }} />
), items, })); ``` **Key points:** - `$checked` automatically syncs - no handler needed - Inline handlers for add/remove operations - **Uses `Cell.equals()` for item identity** - Ternary in `style` attribute works fine - Type inference works in `.map()` - no annotations needed --- ## Level 2: Derived Views Add `computed()` for data transformations: ```typescript import { computed, Default, NAME, pattern, UI } from "commontools"; interface Item { title: string; done: Default; category: Default; } interface Input { items: Default; } export default pattern(({ items }) => { const grouped = computed(() => { const groups: Record = {}; for (const item of items) { const cat = item.category || "Other"; if (!groups[cat]) groups[cat] = []; groups[cat].push(item); } return groups; }); const categories = computed(() => Object.keys(grouped).sort()); return { [NAME]: "By Category", [UI]: (
{categories.map((cat) => (

{cat}

{(grouped[cat] ?? []).map((item) => ( {item.title} ))}
))}
), items, }; }); ``` **Key points:** - `computed()` creates reactive transformations - Direct property access: `grouped[cat]` - Inline null coalescing: `(grouped[cat] ?? [])` --- ## Level 3: Linked Charms Separate patterns sharing data through charm linking: ```bash # Deploy both charms deno task ct charm new ... editor.tsx # Returns: editor-id deno task ct charm new ... viewer.tsx # Returns: viewer-id # Link their data deno task ct charm link ... editor-id/items viewer-id/items ``` Changes in the editor automatically appear in the viewer. ### Cross-Charm Mutations Direct writes to another charm's cells fail with `WriteIsolationError`. Use `Stream.send()`: ```typescript // Charm B: Expose a stream for receiving updates interface Input { items: Cell; addItem: Stream<{ title: string }>; } export default pattern(({ items, addItem }) => { addItem.subscribe(({ title }) => { items.push({ title, done: false }); }); // ... }); // Charm A: Send to Charm B's stream const add = handler((_, { linkedStream }) => { linkedStream.send({ title: "New" }, { onCommit: () => console.log("Sent!") }); }); ``` --- ## Level 4: Pattern Composition Multiple patterns sharing data within a single charm: ```typescript import ShoppingList from "./shopping-list.tsx"; import CategoryView from "./category-view.tsx"; export default pattern(({ items }) => { const listView = ShoppingList({ items }); const catView = CategoryView({ items }); return { [NAME]: "Both Views", [UI]: (
{listView}
{catView}
), items, }; }); ``` Both patterns receive the same `items` cell - changes sync automatically. **When to use which:** - **Pattern Composition**: Multiple views in one UI, reusable components - **Linked Charms**: Independent deployments that communicate --- ## Making Charms Discoverable Export a `mentionable` property to make child charms appear in `[[` autocomplete: ```typescript export default pattern(({ ... }) => { const childCharm = ChildPattern({ ... }); return { [NAME]: "Parent", [UI]:
...
, mentionable: [childCharm], // Makes childCharm discoverable via [[ }; }); ``` For dynamic collections, use a Cell: ```typescript const createdCharms = Cell.of([]); const create = handler((_, { createdCharms }) => { createdCharms.push(ChildPattern({ name: "New" })); }); return { [UI]: Create, mentionable: createdCharms, // Cell is automatically unwrapped }; ``` **Notes:** - Exported mentionables appear in `[[` autocomplete - They do NOT appear in the sidebar charm list - Use this instead of writing to `allCharms` directly --- ## Quick Reference ### When to Use What | Need | Use | |------|-----| | Toggle checkbox | `$checked` (bidirectional) | | Edit text | `$value` (bidirectional) | | Add/remove from array | Inline handler | | Complex/reusable logic | `handler()` | | Transform data | `computed()` | | Filter/sort lists | `computed()` | | Cross-charm mutation | `Stream.send()` | | Make charm discoverable | Export `mentionable` | ### Cell<> in Type Signatures | Type | Meaning | |------|---------| | `items: Item[]` | Read-only, reactive | | `items: Cell` | Read + write (will mutate) | | `items: Default` | Optional with default | --- ## Common Mistakes ### Direct Data Access ```typescript // ❌ Error: reactive reference outside reactive context for (const entry of entries) { ... } // ✅ Wrap in computed() const result = computed(() => { for (const entry of entries) { ... } }); ``` ### Forgetting $ Prefix ```typescript // ❌ One-way only - changes don't sync back // ✅ Bidirectional binding ``` ### Filter/Sort Not Updating ```typescript // ❌ WRONG: Inline filtering in JSX won't update reactively {items.filter(i => !i.done).map(...)} // ✅ CORRECT: Compute outside JSX, then map over the result const active = computed(() => items.filter(i => !i.done)); {active.map(...)} // You CAN map over computed() results! ``` ### Template String Access ```typescript // ❌ Error: reactive reference from outer scope const prompt = `Seed: ${seed}`; // ✅ Wrap in computed() const prompt = computed(() => `Seed: ${seed}`); ``` ### lift() Closure Pattern ```typescript // ❌ Error: reactive reference from outer scope cannot be accessed via closure const result = lift((g) => g[date])(grouped); // ✅ Pass all reactive dependencies as parameters const result = lift((args) => args.g[args.d])({ g: grouped, d: date }); // ✅ Or use computed() instead (handles closures automatically) const result = computed(() => grouped[date]); ``` See [CELLS_AND_REACTIVITY.md](CELLS_AND_REACTIVITY.md) section "lift() and Closure Limitations" for details on frame-based execution and why `computed()` doesn't have this issue. ### Style Syntax ```typescript // ✅ HTML elements - Object syntax
// ✅ Custom elements - String syntax ``` ### Conditional Rendering ```typescript // ❌ Ternary for elements doesn't work {show ?
Content
: null} // ✅ Use ifElse() {ifElse(show,
Content
, null)} // ✅ Ternary IS fine for attributes ``` ### onClick in computed() ```typescript // ❌ Causes ReadOnlyAddressError const ui = computed(() => ( Click )); // ✅ Keep buttons at top level, use disabled for conditional Click ``` --- ## Development Workflow ```bash # Check syntax (fast) deno task ct dev pattern.tsx --no-run # Test locally deno task ct dev pattern.tsx # Deploy deno task ct charm new ... pattern.tsx # Update existing (faster iteration) deno task ct charm setsrc ... --charm CHARM_ID pattern.tsx # Inspect data deno task ct charm inspect ... --charm CHARM_ID ``` **Tips:** - Use `dev` first to catch TypeScript errors - Deploy once, then use `setsrc` for updates - Test one feature at a time --- ## See Also - [CELLS_AND_REACTIVITY.md](CELLS_AND_REACTIVITY.md) - Deep dive on reactivity - [COMPONENTS.md](COMPONENTS.md) - UI component reference - [TYPES_AND_SCHEMAS.md](TYPES_AND_SCHEMAS.md) - Type system details - [DEBUGGING.md](DEBUGGING.md) - Troubleshooting guide