The Entity Model
WildflowerJS was designed to be simple and cohesive. That's 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.
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 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 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:
- 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.
- 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.
- 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 a deliberate optimistic update 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:
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()