A lazy, writable version of memo that unwraps and tracks functions
and promises recursively. Pass any number of stage callbacks — each
receives the resolved output of the previous stage. The body doesn't
run until the result is read; writing to a derived overrides the
computed value until one of its tracked sources changes again.
| name | type | description |
|---|---|---|
...stages |
Array<(prev?) => any> |
one or more functions to run in order. Each stage receives the resolved value of the previous stage (or undefined for the first). Returned functions and promises are unwrapped recursively. |
Returns: a callable signal — call it with no args to read, or pass
a value to override. await works too: a derived is thenable and
resolves once its current pending stage commits.
isResolved(d) reports true once the current run has
committed — and flips back to false while a re-run is pending.
import { derived, signal } from 'pota'
const base = signal(10)
const doubled = derived(() => base.read() * 2)
doubled() // read
doubled(50) // override (until base changes again)
derived(fn) is like memo but writable: d() reads, d(value)
writes. A manual write replaces the computed value until one of fn's
tracked dependencies fires a re-run, at which point the chain takes
over again. Useful for values that are mostly auto-computed but
occasionally need an explicit override.
import { derived, render, signal } from 'pota'
function App() {
const base = signal(10)
const total = derived(() => base.read() * 2)
return (
<div>
<p>total: {total}</p>
<button on:click={() => base.update(n => n + 1)}>
bump base
</button>
<button on:click={() => total(999)}>override total</button>
</div>
)
}
render(App)
derived(f0, f1, f2, ...) runs the input through each stage in turn;
each stage receives the previous stage's resolved value. A dependency
change re-runs the chain from the affected stage onward — earlier
stages keep their cached results. Type into the input — every
keystroke walks the chain to produce a slug.
import { derived, render, signal } from 'pota'
function App() {
const raw = signal(' Hello, World! ')
const cleaned = derived(
() => raw.read(),
s => s.trim(),
s => s.toLowerCase(),
s => s.replace(/[^a-z0-9]+/g, '-'),
)
return (
<div>
<input
prop:value={raw.read}
on:input={e => raw.write(e.currentTarget.value)}
/>
<p>slug: {cleaned}</p>
</div>
)
}
render(App)
derived unwraps promises automatically — each stage's input is
already resolved by the time it runs. isResolved(post)
is false while a fetch is pending — initially and again on every
re-run — so it doubles as a loading flag.
import { derived, isResolved, render, signal } from 'pota'
function App() {
const id = signal(1)
const post = derived(
() => `https://jsonplaceholder.typicode.com/posts/${id.read()}`,
url => fetch(url),
res => res.json(),
)
return (
<div>
<button on:click={() => id.update(n => n + 1)}>next</button>
<p>{() => (isResolved(post) ? post().title : 'loading…')}</p>
</div>
)
}
render(App)
A derived with promises rejects through the reactive scope —
unhandled, the rejection routes to console.error. Wrap the consumer
in <Errored/> so a failed fetch or
res.json() shows a fallback instead. The fallback's reset
re-mounts the children — Post's state is recreated from scratch
(id back to 1), so the retry starts clean.
import { derived, render, signal } from 'pota'
import { Errored } from 'pota/components'
function Post() {
const id = signal(1)
const post = derived(
() => `https://jsonplaceholder.typicode.com/posts/${id.read()}`,
url =>
fetch(url).then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
}),
)
return (
<div>
<button on:click={() => id.update(n => n + 1)}>next</button>
<button on:click={() => id.write(99999)}>break it</button>
<h2>{() => post()?.title ?? 'loading…'}</h2>
</div>
)
}
function App() {
return (
<Errored
fallback={(err, reset) => (
<div>
<p>request failed: {String(err)}</p>
<button on:click={reset}>retry</button>
</div>
)}
>
<Post />
</Errored>
)
}
render(App)