storage(prefix) builds a namespaced signal factory. Every signal it
produces persists under prefix + key in localStorage — falling
back to sessionStorage, and then to an in-memory shim when both are
unavailable (private mode, sandboxed iframes, etc.). The caller picks
the separator: 'my-app:', 'my-app/', or no suffix at all.
Each call returns a plain pota signal object — .read(),
.write(v), and .update(prev => next). The initial value comes from
storage when present, otherwise falls back to the initial argument.
Storage writes are wrapped in try/catch so quota or private-mode
failures don't crash anything — the signal still behaves correctly in
memory.
storage(prefix):
| Argument | Type | Description |
|---|---|---|
prefix |
string |
Prepended to every key; the caller picks the separator. |
Returns: a factory (key, initial) => Signal.
The returned factory:
| Argument | Type | Description |
|---|---|---|
key |
string |
Stored under prefix + key. |
initial |
T |
Optional fallback used when storage has no value for the key. |
Returns: a plain pota signal object.
Reads happen synchronously at construction. Writes go to storage in a
synchronous effect, so the value is persisted by the time write()
returns — not on the next microtask.
Signals built from the same prefix + key within the same document
see each other's writes immediately — no manual subscription needed.
Browser-backed signals also react to the native storage event from
other tabs. If another tab calls localStorage.clear(), every active
signal reverts to its own initial value.
Reload the page and the counter resumes from where you left it, because the value is written to storage synchronously on every update.
import { render } from 'pota'
import { storage } from 'pota/use/storage'
const store = storage('counter-demo:')
const count = store('count', 0)
function App() {
return (
<div>
<p>persisted count: {count.read}</p>
<button on:click={() => count.update(n => n + 1)}>
increment
</button>
<button on:click={() => count.write(0)}>reset</button>
<p>
<small>reload the page — the count survives</small>
</p>
</div>
)
}
render(App)
storage(prefix) returns a factory you can call many times. Two
signals built from the same prefix + key stay in sync within the
same document — fan-out happens without any manual subscription.
import { render } from 'pota'
import { storage } from 'pota/use/storage'
const settings = storage('settings:')
// two independent calls, same key — they stay in sync
const fontA = settings('font-size', 14)
const fontB = settings('font-size', 14)
function App() {
return (
<div>
<p>signal A reads: {fontA.read}px</p>
<p>signal B reads: {fontB.read}px</p>
<button on:click={() => fontA.update(n => n + 1)}>
bump from A
</button>
<button on:click={() => fontB.write(14)}>reset from B</button>
<p>
<small>
both readouts update together — fan-out happens inside the
same document
</small>
</p>
</div>
)
}
render(App)
Browser-backed signals follow the native storage event, so open this
page in a second tab and toggle the checkbox — both tabs update in
lockstep.
import { render } from 'pota'
import { storage } from 'pota/use/storage'
const prefs = storage('prefs:')
const dark = prefs('dark', false)
function App() {
return (
<div
style={{
background: () => (dark.read() ? '#111' : '#fafafa'),
color: () => (dark.read() ? '#eee' : '#111'),
padding: '1rem',
}}
>
<label>
<input
type="checkbox"
prop:checked={dark.read}
on:change={e => dark.write(e.currentTarget.checked)}
/>
dark mode
</label>
<p>
<small>
open this page in a second tab and toggle the checkbox —
both tabs follow each other via the native storage event
</small>
</p>
</div>
)
}
render(App)