Overview

useState is the right primitive when local component state is naturally an object — fields, methods, and computed getters together. It returns [wrapper, update]: a read-tracked proxy and the underlying atom’s update handle.

Methods on the initial object are automatically wrapped so calling state.incr() runs the method and notifies subscribers — you do not call update() inside those methods. Direct assignment (state.count = 5) is also reactive.

Getters are version-cached: they recompute when state changes, not on every template read. Prefer this over a pile of separate atoms when the fields always move together.

Compared to atom / setter

Member
Status
Notes
useState({…}) object proxy One reactive bag with methods + getters; component-local.
atom(value) single value Best for primitives or values shared via props / effects.
setter manual notify Plain let + re-render; no property-level tracking.

Async anti-pattern

// ✗ throws — async methods not supported
const [bad] = useState({
  async fetchData() { /* … */ }
});

// ✓ keep async outside useState
const load = asyncAction(async () => { /* … */ });

useState — object state with methods

Methods defined on the state object are automatically wrapped to trigger a re-render after they run. Getters work as derived values and are re-evaluated on each render.

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

const App = () => {
const [form] = useState({
name: '',
email: '',
submitted: false,
// Methods auto-wrapped to trigger re-render
submit() {
this.submitted = true;
},
reset() {
this.name = '';
this.email = '';
this.submitted = false;
},
get isValid() {
return this.name.length > 0 && this.email.includes('@');
},
});

return () => html`
<div class="m-col m-gap">
${form.submitted
? html`
<div class="card card-body center">
<span class="tag tag-success m-b-10">Submitted!</span>
<p class="muted">Name: <strong>${form.name}</strong></p>
<p class="muted m-b-10">Email: <strong>${form.email}</strong></p>
<button class="btn-ghost" @click=${form.reset}>Reset</button>
</div>
`
: html`
<input .value=${form.name} @input=${e => form.name = e.target.value} placeholder="Name" />
<input .value=${form.email} @input=${e => form.email = e.target.value} placeholder="Email" type="email" />
<button class="btn-primary" ?disabled=${!form.isValid} @click=${form.submit}>Submit</button>

Direct assign + update()

Assign properties on the proxy for single-field edits. Use update(fn) to batch several mutations into one notify.

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

const App = () => {
const [state, update] = useState({
count: 0,
step: 1,
get doubled() { return this.count * 2; },
});

const resetBoth = () => update((s) => {
s.count = 0;
s.step = 1;
});

return () => html`
<div class="m-col m-gap">
<div class="m-grid m-grid-cols-3 m-gap">
<div class="card card-body center">
<span class="overline">Count</span>
<span class="card-title">${state.count}</span>
</div>
<div class="card card-body center">
<span class="overline">Doubled</span>
<span class="card-title">${state.doubled}</span>
</div>
<div class="card card-body center">
<span class="overline">Step</span>
<span class="card-title">${state.step}</span>
</div>
</div>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${() => { state.count += state.step; }}>+step</button>
<button @click=${() => { state.step = state.step === 1 ? 5 : 1; }}>
step=${state.step}
</button>
<button class="btn-ghost" @click=${resetBoth}>update() reset</button>