The Entity Model

WildflowerJS keeps its parts consistent with each other. That is most visible in its four building blocks for reactive behavior: components, stores, plugins, and pools all share the same declaration shape (state, computed, methods, lifecycle). They differ in specialization: what each one uniquely adds, and what it gives up. The fifth kind, the query, inverts the authorship: the other four let you author the shape and supply the data, while a query fixes the shape (a store the engine declares and owns) and lets you author the source it stays filled from.

One shape has five specializations, each suited to a different job. Pick by what your code needs to do. The query is the fifth: a store the engine owns and keeps filled from an external source; see below.
A note on terminology. "Entity" appears in two senses. Broadly, every component, store, plugin, and pool is an entity: one of the four building blocks of the framework. Each gets its own reactive graph, the engine that powers reactivity. Specifically, the items inside a pool are also called entities (declared via the pool's entity: block), but those items share the pool's single reactive graph rather than each having their own. A pool with 2600 items is one reactive graph, not 2600. That's what makes pools fast at scale.

Shared shape

Whether you're writing a component, store, plugin, or pool, every declaration uses the same four things:

  • state: the entity's data fields
  • computed: values derived from state (declared as functions)
  • methods: behaviors, declared as top-level functions on the definition object
  • lifecycle hooks: init(), tick(dt), destroy(), and so on

Learn the shape once in any of the four, and you know it everywhere.

Capability matrix

Where the five kinds differ.

Capability Pool entity Store Query Component Plugin
Declare state ✅ (defaults, merge-on-add) ✅ (pre-authored: rows + sync flags)
Declare computed ✅ (uncached getters) ✅ (reactive, cached) ✅ (pre-authored: count) ✅ (reactive, cached) ✅ (reactive, cached)
Declare methods ✅ (pre-authored: refresh, invalidate)
init / tick / destroy hooks via pool (onAdd, onRemove) automatic (active-while-observed)
Proxy-reactive state (push-based) ❌ (plain JS, pull-based)
Subscribe to stores ✅ (cross-store) ❌ (components subscribe to it)
localStorage persistence (storageKey) n/a (the source is the persistence)
Owns DOM / template via pool template via data-query markup
data-action event routing via template
Framework extension hooks (custom directives, globals)
Standing external source (freshness ladder, race discipline, SSR adoption)
Multiplicity many (keyed by id) singleton (per name) singleton (per name) many (per mounted element) singleton (per name)

How to choose

A decision tree in prose:

  1. Are you extending the framework itself? (custom directives, adding global methods, hooking into the framework's lifecycle) → Plugin.
  2. Do you have many similar things to render at high frequency? (particles, enemies, table rows, hundreds of reactive items updating per frame) → Pool entity. You give up Proxy reactivity; you gain cheap multi-instance rendering.
  3. Does the data live somewhere else and need to stay current? (a server endpoint, IndexedDB, anything with a source of truth outside the page) → Query FULL. You give up write ownership; you gain a store the engine keeps filled, with freshness, race discipline, and SSR adoption handled.
  4. Do you need global state shared across components? (user session, cart, settings, anything that survives navigation) → Store. You give up DOM ownership; you gain singleton addressability and optional persistence.
  5. Anything else.Component. The default choice for UI-facing, per-element reactive logic.
Can I mix them? Yes. A component can own a pool, subscribe to several stores, and be extended by a plugin, all in the same application. The specializations compose.

Queries: a store with a source FULL

A query is the one kind you don't declare with the shared shape directly. wildflower.query('inventory', { from, key, refresh }) declares a source contract, and the engine declares the store for you: state holding rows and the sync flags, a count computed, and refresh()/invalidate() methods, all in the standard store idiom. The result is a full citizen of the store world. $inventory.* bindings, getStore-grade auto-tracking via getQuery, and subscribe: declarations all work because it is a real store in the same registry (queries and stores share one namespace, and a name collision is refused with a dev warning).

The difference is ownership. The engine writes the store on every sync, so application writes are a contract violation rather than a hard error: markup writes are already refused ($store.path is read-only in data-model, WF-501), and a JS write from application code draws a dev warning (WF-950) while still landing, since an optimistic update from application code is legitimate. The supported write path is: mutate the data source, then invalidate().

The other difference is arity. A store carries many named fields, so its name is a container distinct from the variables inside it: warehouse.inventory, warehouse.staff, warehouse.settings. A query carries a single dataset, always rows, wrapped in the sync flags, so there is no separate variable to name. The query's name is the dataset's name: $inventory is the query (state, flags, methods), and $inventory.rows is the inventory (the list, record, or scalar it holds). That is why a list-shape data-query="inventory" renders the inventory without .rows appearing anywhere; the engine targets rows for you (a record shape rewrites bare paths to rows.0.* the same way). Name a query for the data it delivers, a noun like inventory or orders, never for the fetch (fetchInventory) or the source (inventoryApi), because that name becomes how the whole app refers to the data.

What "pull-based" means for pool entities

The one row in the matrix that trips up new users is "Proxy-reactive state." Every other entity kind uses push reactivity: mutating this.count++ immediately triggers dep tracking, re-runs computed, and updates bindings. Pool entities don't.

Pool entities are plain JavaScript objects. Mutating entity.hp = 50 does nothing immediately. The pool re-reads every entity once per animation frame and syncs the DOM in a batch. This is what lets pools render thousands of entities without per-property Proxy overhead. The cost is that you don't get synchronous, per-mutation reactivity at the entity level.

For most pool use cases (animation, real-time data, games) this tradeoff is correct. See Why Pools? for the full story.

Side-by-side declarations

The same shape, declared five ways. Four you author yourself; the query names a source and the engine authors the shape:

Component
wildflower.component('counter', {
    state: { count: 0 },
    computed: {
        doubled() { return this.count * 2 }
    },
    increment() { this.count++ },
    init() { /* ... */ }
})
Store
wildflower.store('cart', {
    state: { items: [] },
    computed: {
        total() { return this.items.length }
    },
    add(item) { this.items.push(item) },
    init() { /* ... */ }
})
Plugin
wildflower.plugin('timer', {
    state: { elapsed: 0 },
    computed: {
        seconds() { return this.elapsed / 1000 }
    },
    reset() { this.elapsed = 0 },
    init() { /* ... */ }
})
Pool Entity
pools: {
    enemies: {
        entity: {
            state: { hp: 100 },
            computed: {
                isDead() { return this.hp <= 0 }
            },
            takeDamage(n) { this.hp -= n }
        }
    }
}
Query
wildflower.query('inventory', {
    from: '/api/inventory',
    key: 'id',
    refresh: ['focus', 'etag:60']
})
// the engine authors the shape:
// state:    rows + sync flags
// computed: count
// methods:  refresh(), invalidate()
Continue Learning: Deep-dive pages for each kind: Components, Stores, Queries, Plugins, and Entity Pools.
Lite Tower Defense (data-pool)

Enemies, towers, and projectiles as pool entities, each with its own state and methods, flushed on requestAnimationFrame.