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.
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.
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 fieldscomputed: 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:
- Are you extending the framework itself? (custom directives, adding global methods, hooking into the framework's lifecycle) → Plugin.
- 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.
- 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.
- 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.
- 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
workerOnlyso it never crosses. - Anything else. → Component. The default choice for UI-facing, per-element reactive logic.
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:
wildflower.component('counter', {
state: { count: 0 },
computed: {
doubled() { return this.count * 2 }
},
increment() { this.count++ },
init() { /* ... */ }
})
wildflower.store('cart', {
state: { items: [] },
computed: {
total() { return this.items.length }
},
add(item) { this.items.push(item) },
init() { /* ... */ }
})
wildflower.plugin('timer', {
state: { elapsed: 0 },
computed: {
seconds() { return this.elapsed / 1000 }
},
reset() { this.elapsed = 0 },
init() { /* ... */ }
})
pools: {
enemies: {
entity: {
state: { hp: 100 },
computed: {
isDead() { return this.hp <= 0 }
},
takeDamage(n) { this.hp -= n }
}
}
}
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()
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
Enemies, towers, and projectiles as pool entities, each with its own state and methods, flushed on requestAnimationFrame.