signalize-memos
Computed/derived signals for @spearwolf/signalize: createMemo for cached computations that automatically update when dependencies change. Use when creating derived values that depend on other signals.
SKILL.md
| Name | signalize-memos |
| Description | Computed/derived signals for @spearwolf/signalize: createMemo for cached computations that automatically update when dependencies change. Use when creating derived values that depend on other signals. |
name: signalize-memos description: 'Computed/derived signals for @spearwolf/signalize: createMemo for cached computations that automatically update when dependencies change. Use when creating derived values that depend on other signals.'
Signalize Memos
Overview
Memos are computed signals - cached values that automatically recalculate when their dependencies change. They combine the reactivity of effects with the read interface of signals.
import {createMemo, createSignal} from '@spearwolf/signalize';
const count = createSignal(0);
const doubled = createMemo(() => count.get() * 2);
doubled(); // 0
count.set(5);
doubled(); // 10 (automatically updated)
Quick Start
const firstName = createSignal('John');
const lastName = createSignal('Doe');
const fullName = createMemo(() => {
return `${firstName.get()} ${lastName.get()}`;
});
fullName(); // 'John Doe'
firstName.set('Jane');
fullName(); // 'Jane Doe'
Lazy vs Non-Lazy Memos
Non-Lazy (Default)
A non-lazy memo acts as a computed signal: it recomputes immediately when dependencies change — even before anyone reads it. This means effects and other memos that depend on it are automatically notified, just like with a regular signal.
let computeCount = 0;
const doubled = createMemo(() => {
computeCount++;
return count.get() * 2;
});
// computeCount = 1 (computed on creation)
count.set(5);
// computeCount = 2 (recomputed immediately)
doubled(); // Returns cached value, no recompute
doubled(); // Still cached
// computeCount = 2
Key behavior: Because the memo eagerly updates its value, effects that read it will re-run when the value changes:
const count = createSignal(1);
const doubled = createMemo(() => count.get() * 2);
createEffect(() => {
console.log('Doubled:', doubled());
});
// => "Doubled: 2"
count.set(5);
// memo recalculates (now 10), then the effect re-runs
// => "Doubled: 10"
Use when:
- The memo is a dependency of effects or other memos
- You need it to behave like a computed signal in a reactive chain
- The value should always be up-to-date
Lazy
A lazy memo does not react to dependency changes on its own. It defers recomputation until the memo is actually read. The recalculation happens at the latest possible moment.
Because it does not eagerly update, effects that depend on a lazy memo will not automatically re-run when the memo's source dependencies change.
let computeCount = 0;
const doubled = createMemo(
() => {
computeCount++;
return count.get() * 2;
},
{lazy: true},
);
// computeCount = 0 (not computed yet!)
doubled();
// computeCount = 1 (computed on first read)
count.set(5);
// computeCount = 1 (NOT recomputed yet)
doubled();
// computeCount = 2 (recomputed on read)
Use when:
- The computation is expensive and the memo might not be read after every change
- The memo is consumed on-demand rather than observed by effects
- Dependencies change frequently but the value is consumed infrequently
createMemo Options
createMemo(factory, options?)
| Option | Type | Default | Description |
|---|---|---|---|
lazy | boolean | false | Defer computation until read |
attach | object | SignalGroup | - | Attach to group for lifecycle |
name | string | symbol | - | Named signal in group |
priority | number | 1000 | Execution priority (higher = first) |
attach and name
const group = SignalGroup.findOrCreate(this);
const fullName = createMemo(() => `${firstName.get()} ${lastName.get()}`, {
attach: group,
name: 'fullName',
});
// Later: access by name
group.signal('fullName');
priority
Memos have default priority 1000, higher than effects (default 0). This ensures memos compute before effects that depend on them.
// Custom priority
const critical = createMemo(compute, {priority: 2000}); // Runs first
const normal = createMemo(compute); // Priority 1000
const deferred = createMemo(compute, {priority: 500}); // Runs later
Caching Behavior
Memos cache their result and only recompute when dependencies actually change:
let computeCount = 0;
const expensive = createMemo(() => {
computeCount++;
return heavyCalculation(input.get());
});
expensive(); // computeCount = 1
expensive(); // computeCount = 1 (cached)
expensive(); // computeCount = 1 (still cached)
input.set(newValue);
expensive(); // computeCount = 2 (recomputed)
Memo vs Effect
| Aspect | Memo | Effect |
|---|---|---|
| Returns value | YES | NO |
| Caches result | YES | NO |
| Can be read | YES (like signal) | NO |
| Side effects | Avoid | Expected |
| Use for | Derived data | Actions/DOM updates |
// MEMO: Derived data
const total = createMemo(() =>
items.get().reduce((sum, i) => sum + i.price, 0),
);
// EFFECT: Side effect
createEffect(() => {
document.title = `Total: ${total()}`;
});
Chained Memos
Memos can depend on other memos:
const items = createSignal([{price: 10}, {price: 20}]);
const taxRate = createSignal(0.1);
const subtotal = createMemo(() =>
items.get().reduce((sum, i) => sum + i.price, 0),
);
const tax = createMemo(() => subtotal() * taxRate.get());
const total = createMemo(() => subtotal() + tax());
total(); // 33 (30 + 3)
Cleanup
Memos are destroyed like signals:
const memo = createMemo(() => compute());
// Using destroySignal
destroySignal(memo);
// Or via SignalGroup
const group = SignalGroup.findOrCreate(obj);
createMemo(compute, {attach: group});
group.clear(); // Destroys all attached memos
Common Patterns
Filtered List
const items = createSignal([1, 2, 3, 4, 5]);
const filter = createSignal((n: number) => n > 2);
const filtered = createMemo(() => items.get().filter(filter.get()));
Sorted Data
const data = createSignal([{name: 'B'}, {name: 'A'}]);
const sortKey = createSignal('name');
const sorted = createMemo(() =>
[...data.get()].sort((a, b) =>
a[sortKey.get()].localeCompare(b[sortKey.get()]),
),
);
Expensive Computation with Lazy
const searchQuery = createSignal('');
const allItems = createSignal([
/* large dataset */
]);
// Only compute when actually needed
const searchResults = createMemo(
() => {
const query = searchQuery.get().toLowerCase();
return allItems
.get()
.filter((item) => item.name.toLowerCase().includes(query));
},
{lazy: true},
);
Pitfalls to Avoid
1. Side effects in memos
// BAD - memos should be pure
const bad = createMemo(() => {
console.log('Computing...'); // Side effect!
return value.get() * 2;
});
// GOOD - use effect for side effects
const good = createMemo(() => value.get() * 2);
createEffect(() => {
console.log('Value doubled:', good());
});
2. Expecting lazy memo to be current
const memo = createMemo(compute, {lazy: true});
signal.set(newValue);
// memo() might return OLD value until you call it!
3. Forgetting cleanup
// Always destroy memos or use SignalGroup
const memo = createMemo(compute);
// ... later ...
destroySignal(memo); // Don't forget!
See Also
- Developer Guide - Comprehensive guide to all features
- Full API Reference - Complete API documentation
- Cheat Sheet - Quick reference