Overview

iAtom (also exported as signal) is an immutable atom built on atom({ freeze: true }). Plain atom values are mutable by default — you can call .update() or mutate objects in place. With iAtom, every value written via .set() is deep-frozen, including the initial value and nested objects/arrays.

That turns accidental mutation into a runtime error (TypeError: Cannot assign to read only property in strict mode) instead of a silent bug where the UI never updates because nothing notified the atom.

The read API matches atom: call the atom, use .get(), or read .val. The write path is replace-only: pass a new value or an updater that returns a new object. There is no .update(). .reset() restores the frozen initial value (same reference — no clone). lock / unlock / freeze are inherited from atom.

API differences from atom

Member
Status
Notes
() / .get() / .val same Reactive reads work identically.
.set(value) same + freeze Deep-freezes the incoming value before storing.
.set(prev => next) same + freeze Updater fn — use spread to derive new objects.
.update() removed Does not exist on iAtom — use .set() with a new value.
.reset() iAtom only Restores the frozen initial reference (no clone).
.lock / .unlock inherited Same passcode semantics as atom.

Immutable config object

Every set() call deep-freezes the new value. Use spread in the updater to change one field without mutating the previous object.

import { html, atom, iAtom, renderApp } from 'mates';

const App = () => {
const config = iAtom({ theme: 'dark', fontSize: 16, lang: 'en' });
const log = atom([]);

const update = (key, val) => {
config.set(prev => ({ ...prev, [key]: val }));
log.set(prev => [`${key}${val}`, ...prev].slice(0, 4));
};

return () => html`
<div class="m-col m-gap">
<div class="card card-body m-col m-gap">
<span class="label">Current config (frozen object)</span>
<pre class="mono">
${JSON.stringify(config(), null, 2)}</pre>
<span class="hint">isFrozen: ${Object.isFrozen(config())}</span>
</div>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-ghost" @click=${() => update('theme', config().theme === 'dark' ? 'light' : 'dark')}>
Toggle theme
</button>
<button class="btn-ghost" @click=${() => update('fontSize', config().fontSize + 2)}>
Font +2
</button>
<button class="btn-ghost" @click=${() => update('lang', config().lang === 'en' ? 'fr' : 'en')}>
Toggle lang
</button>
</div>
<div class="card card-body">
<span class="overline">Change log</span>
${log().length === 0
? html`<div class="mono subtle">No changes yet…</div>`
: log().map(e => html`<div class="mono">${e}</div>`)
}

Mutation fails — replace instead

Try a direct property write (blocked by freeze), then fix it with set() + spread. Nested objects are frozen too.

import { html, iAtom, renderApp } from 'mates';

const App = () => {
const settings = iAtom({ theme: 'dark', nested: { debug: true } });
const message = iAtom('Try mutating — then replace with set()');

const tryMutate = () => {
try {
settings().theme = 'light';
message.set('Mutation appeared to work (non-strict) — but Object.isFrozen is still true');
} catch (e) {
message.set('Blocked: ' + (e && e.message ? e.message : String(e)));
}
};

const replaceCorrectly = () => {
settings.set(prev => ({
...prev,
theme: prev.theme === 'dark' ? 'light' : 'dark',
nested: { ...prev.nested },
}));
message.set('Replaced via set() + spread — the right pattern');
};

return () => html`
<div class="m-col m-gap">
<div class="card card-body m-col m-gap">
<pre class="mono muted">
${JSON.stringify(settings(), null, 2)}</pre>
<span class="hint">
frozen: ${Object.isFrozen(settings())} / nested: ${Object.isFrozen(settings().nested)}
</span>
</div>
<p class="muted">${message()}</p>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-danger" @click=${tryMutate}>Try direct mutation</button>

reset() to frozen initial

After edits, reset() restores the exact initial frozen reference.

import { html, iAtom, renderApp } from 'mates';

const App = () => {
const cfg = iAtom({ api: 'https://v1.example', retries: 3 });

return () => html`
<div class="m-col m-gap">
<pre class="mono">${JSON.stringify(cfg(), null, 2)}</pre>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${() =>
cfg.set(prev => ({ ...prev, api: 'https://v2.example', retries: prev.retries + 1 }))
}>Bump version</button>
<button @click=${() => cfg.reset()}>reset()</button>
</div>
<p class="muted m-t-5">reset() restores the frozen initial reference — no clone.</p>
</div>
`;
};
renderApp(App, document.getElementById('app'));