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. The sixth, the thread, moves where the shape runs: the same declaration, evaluated in a Web Worker, with the page reading a mirror of it. It ships as an extension rather than in the framework, and is marked that way throughout this page.

One shape has six 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. The thread is the sixth: a store that runs in a Web Worker, shipped as an extension. Both are 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.
An entity does not need a page. An entity is a declaration plus a reactive graph, and neither requires a document, so one can run somewhere other than the page. The Threads extension loads the framework inside a Web Worker and registers your definition there through wildflower.store(), so what runs is a real store with its own graph: computeds track dependencies, watchers fire, lifecycle hooks run, all off the main thread. The page holds a second store under the same name, the mirror, kept in step by messages. What makes it a kind of its own rather than a store in an unusual place is the workerOnly block, which no other declaration has and which says where each part of the state lives.

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 six kinds differ. The last column is an extension, loaded as a second script after the framework; the others are in every build that carries them.

Capability Pool entity Store Query Component Plugin Thread extension
Declare state (defaults, merge-on-add) (pre-authored: rows + sync flags) (plus workerOnly: which keys stay in the worker)
Declare computed (uncached getters) (reactive, cached) (pre-authored: count) (reactive, cached) (reactive, cached) (reactive, cached; evaluated in the worker)
Declare methods (pre-authored: refresh, invalidate) (called on the mirror, return promises)
init / tick / destroy hooks via pool (onAdd, onRemove) automatic (active-while-observed) (tick runs in the worker, keeps going when hidden)
Proxy-reactive state (push-based) (plain JS, pull-based) in the worker; the mirror is written by patch
Subscribe to stores (cross-store) (components subscribe to it) (its realm holds only itself)
localStorage persistence (storageKey) n/a (the source is the persistence) (a worker has no localStorage)
Owns DOM / template via pool template via data-query markup (no document in the worker)
Declare pools n/a (it is the pool) (data only, stepped by its tick) (its collection is rows) (rendered through data-pool) (data only) (data only, in the worker; computeds over them cross, the entities do not)
data-action event routing via template (the mirror is bound like any store)
Framework extension hooks (custom directives, globals)
Standing external source (freshness ladder, race discipline, SSR adoption)
Evaluates off the main thread (state and computeds in a Worker) (the page reads a mirror, possibly one message behind)
Multiplicity many (keyed by id) singleton (per name) singleton (per name) many (per mounted element) singleton (per name) 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. When several components need the same collection, or it must stay after the component showing it is destroyed, declare the pool in a store instead. A store's pool holds data only, and each component reads it and draws it its own way.
  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. Is the work heavy enough to freeze the page? (filtering hundreds of thousands of rows on every keystroke, parsing a large file, a simulation step) → Thread, which is an extension rather than part of the framework. You give up synchronous method returns and a guaranteed-fresh read; you gain state and computeds evaluated off the main thread, with the heavy data declared workerOnly so it never crosses.
  6. 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 six ways. Four you author yourself, the query names a source and the engine authors the shape, and the thread is one you author that runs somewhere else:

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()
Thread extension
wildflower.thread('orders', {
    state: { rows: [], params: { q: '' } },
    workerOnly: ['rows'],   // stays in the worker
    computed: {
        count() { return this.rows.length }
    },
    async load(url) { /* runs there */ }
})
// the page reads a mirror of it
Continue Learning: Deep-dive pages for each kind: Components, Stores, Queries, Plugins, Entity Pools, and Threads (an extension).
Lite Tower Defense (data-pool)

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