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 reactivitythis.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:

  1. raw state
  2. 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.