State
Reactive state
this.state is a shallow reactive Proxy.
this.state.count = this.state.count + 1
Any assignment to a top-level key schedules a re-render.
Flat proxy
Micra tracks top-level property writes only.
Works:
this.state.count = 2
this.state.user = { name: 'Ana' }
Does not trigger a render by itself:
this.state.user.name = 'Ana'
this.state.items.push(newItem)
Replace the parent value instead.
Array replacement pattern
Replace arrays instead of mutating them in place.
this.state.items = [...this.state.items, nextItem]
this.state.items = this.state.items.filter(item => item.id !== id)
this.state.items = this.state.items.map(item =>
item.id === id ? { ...item, name: 'Updated' } : item,
)
This is the recommended pattern for data-each.
Nested objects
Nested objects are fine, but replace the top-level key when you change them.
this.state.filters = {
...this.state.filters,
query: 'billing',
}
Think of state as a flat set of reactive entry points.
Path-write sugar: this.set() and dot-path data-model
Hand-spreading nested objects on every form field gets noisy. For that, use the path sugar — it does the spread-and-reassign-the-top-level-key for you, so the shallow proxy still fires a render:
this.set('filters.query', 'billing')
// ≡ this.state.filters = { ...this.state.filters, query: 'billing' }
<input data-model="filters.query" /> <!-- two-way bind to a nested path -->
<input data-model="user.address.city" />
This is sugar over the shallow proxy, not deep reactivity — this.set() and
path data-model reconstruct the intermediate objects and reassign the
top-level key (filters, user). A bare this.state.filters.query = x still
won’t render. Reading nested paths in expressions (data-text="user.address.city")
works directly.
TypeScript inference with defineComponent
defineComponent() returns the definition unchanged, but helps TypeScript infer the state shape.
import * as Micra from 'micra.js'
const counter = Micra.defineComponent({
state: { count: 0 },
increment() {
this.state.count++
},
reset() {
this.state.count = 0
},
})
Micra.define('counter', counter)
Inside methods, this.state.count is typed as number.
exprState
During render, Micra evaluates directive expressions against an internal proxy often described as exprState.
It resolves properties in this order:
- raw state
- instance methods and helpers
That means expressions can read state and call methods:
<span data-text="count"></span>
<time data-text="formatDate(createdAt)"></time>
formatDate(value: string) {
return new Date(value).toLocaleString()
}
The expression layer is read-only in practice. Update data through this.state, not through expressions.
Force rendering
You normally do not need to call this.render(). State writes schedule rendering automatically.
Use this.render() only when you need an immediate synchronous refresh after non-state work.