Advanced Pools LITE+
Lifecycle hooks, animation, event handling, static pools, bulk operations, and shared props: the features that make pools a complete rendering primitive.
Pool Lifecycle Hooks
Pools support lifecycle hooks for managing external resources (physics bodies, audio sources, network connections) tied to entity lifetime:
wildflower.component('physics-demo', {
pools: {
balls: {
onAdd: 'onBallAdd', // called after entity added
onRemove: 'onBallRemove', // called before individual removal
onClear: 'onBallsClear' // called once on bulk clear (skips onRemove)
}
},
onBallAdd(item) {
// Create a physics body and attach it to the entity
item._body = PhysicsEngine.createBody(item.x, item.y, item.radius);
},
onBallRemove(item) {
// Clean up physics body + play sound effect
PhysicsEngine.destroyBody(item._body);
SFX.play('clink');
},
onBallsClear(items) {
// Efficient bulk teardown: no per-item sounds
PhysicsEngine.clearAll();
}
})
data-action: the framework looks up the method on the component and binds this automatically.
onClear fallback: If onClear is not defined, pool.clear() fires onRemove for each item individually. If onClear IS defined, onRemove is skipped during clear, leaving bulk cleanup to the developer.
The tick(dt) Lifecycle Hook
Components that define a tick(dt) method get it called automatically once per animation frame. The framework manages the requestAnimationFrame loop. You never need to set one up or tear one down.
wildflower.component('boids', {
pools: { boids: {} },
init() {
// ... spawn entities ...
},
tick(dt) {
// dt = milliseconds since last frame (clamped to 250ms)
var t = dt / 16.67; // normalize: t≈1.0 at 60fps
for (const b of this.pools.boids.items) {
b.x += b.vx * t;
b.y += b.vy * t;
}
// Pool flush happens automatically right after tick returns
}
// No destroy() needed: framework cleans up the rAF loop
});
tick(dt) also receives a second argument now (performance.now() timestamp) for components that need absolute time.
tick(dt) on all tickable components FIRST, then flushes all pool bindings to the DOM. This means entity mutations made in tick are visible in the DOM on the same frame.
tick(dt) works with or without pools. A component with tick but no data-pool still gets the rAF loop, useful for canvas animations, Three.js scenes, or any per-frame logic.
Tip: For HUD elements outside the pool template (FPS counters, score displays), use direct DOM writes like document.getElementById('fps').textContent = fps. This avoids reactive overhead for values that update every frame. See Pool Performance for details.
Event Handling
Pool templates support data-action inside templates. Actions are routed to entity methods first (with this bound to the clicked entity) and fall back to component methods if no entity method matches the name.
Prefer entity methods when the handler only mutates the entity itself: no id lookup, no parent-state plumbing:
<div data-pool="tasks" data-key="id">
<template>
<div class="task">
<span data-bind="title"></span>
<button data-action="toggleDone">Done</button>
<button data-action="removeTask">Delete</button>
</div>
</template>
</div>
wildflower.component('tasks-demo', {
pools: {
tasks: {
entity: {
state: { done: false },
// Entity method: `this` is the task
toggleDone() { this.done = !this.done; }
}
}
},
// Component method: entity methods can't remove themselves from the pool,
// so removal lives at the component level and falls back via data-action.
removeTask(item) { this.pools.tasks.remove(item.id); }
});
For multi-target event delegation (different actions for different sub-elements of a template), data-pool-action on the container accepts selector-specific bindings. See Pool API for the full format reference.
Real-World Example: Game Entities
Here is a pattern from the WildflowerJS Tower Defense demo, showing multiple pools working together: enemies, projectiles, towers, and loot drops, all rendering at native frame rate. Each entity declares its shape via an entity block (see Defining an Entity) so the spawn code stays focused on the instance-specific values.
<div data-component="tower-defense">
<div class="game-field">
<!-- Each pool manages its own entity type -->
<div data-pool="towers" data-key="index">
<template>
<div data-bind-class="towerClass"
data-bind-style="{ left: x + 'px', top: y + 'px' }">
<img data-bind-attr="{ src: imgSrc }" width="48" height="48">
</div>
</template>
</div>
<div data-pool="enemies" data-key="id">
<template>
<!-- className derived by the entity.computed below -->
<div data-bind-class="className"
data-bind-style="{ left: x + 'px', top: y + 'px',
width: w + 'px', height: h + 'px' }">
<img data-bind-attr="{ src: imgSrc, width: w, height: h }">
</div>
</template>
</div>
<div data-pool="projectiles" data-key="id">
<template>
<div class="projectile"
data-bind-style="{ left: x + 'px', top: y + 'px' }">
<img data-bind-attr="{ src: imgSrc }">
</div>
</template>
</div>
<div data-pool="loots" data-key="id">
<template>
<div class="loot" data-bind-style="{ left: x + 'px', top: y + 'px' }">
<img data-bind-attr="{ src: imgSrc }" width="14" height="14">
</div>
</template>
</div>
</div>
</div>
wildflower.component('tower-defense', {
state: { nextId: 1 },
pools: {
enemies: {
entity: {
// Defaults shared across every enemy
state: { speedX: 0, speedY: 1, eState: 'follow', hpPct: 100 },
// className derived from state, flips to "enemy dying"
// when takeDamage() sets eState. No imperative class stitching.
computed: {
className() { return this.eState === 'dying' ? 'enemy dying' : 'enemy'; }
},
// `this` is the enemy entity
takeDamage(n) {
this.hp -= n;
if (this.hp <= 0) this.eState = 'dying';
}
}
},
projectiles: {},
loots: {},
towers: {}
},
spawnEnemy(type, x, y) {
// Only instance-specific fields. Defaults and derivations are declared above.
this.pools.enemies.push({
id: this.nextId++,
x, y,
hp: type.maxHp,
maxHp: type.maxHp,
imgSrc: type.sprite,
w: type.width,
h: type.height
});
},
// Called automatically each frame: no rAF setup needed
tick(dt) {
var t = dt / 16.67; // normalize for frame-rate independence
// Update all enemies: direct property mutation
for (const e of this.pools.enemies) {
e.y += e.speedY * t;
}
// Update all projectiles
for (const p of this.pools.projectiles) {
p.x += p.vx * t;
p.y += p.vy * t;
}
// Remove dead enemies (after their dying animation finishes)
this.pools.enemies.forEach(e => {
if (e.eState === 'dying' && performance.now() - e._diedAt > 600) {
this.pools.enemies.remove(e.id);
}
});
}
});
Static Pools
For collections that don't need per-frame updates, add the data-pool-static boolean attribute. This skips the rAF flush loop entirely. Items are rendered synchronously on add() and update(), with zero idle CPU cost.
<!-- Static pool: renders on add/update only, no rAF loop -->
<tbody data-pool="users" data-key="id" data-pool-static>
<template>
<tr>
<td data-bind="name"></td>
<td data-bind="status"
data-bind-class="status === 'active' ? 'badge active' : 'badge inactive'"></td>
</tr>
</template>
</tbody>
// Populate: items render immediately (bulk add via array)
this.pools.users.add(data);
// Update: DOM reflects the change synchronously
this.pools.users.update('user-42', { status: 'active' });
// Direct property mutation does NOT update DOM in static pools.
// Always use pool.update() to apply changes.
data-pool-static with no value makes the entire pool static (passive). data-pool-static="propName" with a value is a different feature: it marks individual entities as static within a live pool based on an entity property.
Bulk Add
Pass an array to pool.add() to insert multiple items in a single DOM operation via DocumentFragment. This is significantly faster than adding items one at a time for large collections.
// Single add (object)
this.pools.items.add({ id: 1, name: 'Alice' });
// Bulk add (array): single DOM operation
this.pools.items.add([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Carol' }
]);
Bulk add works with both live and static pools, fires onAdd lifecycle hooks for each item, and skips items with duplicate keys.
Pool Props
Pool props are shared data injected by the parent component, available to all pool item expressions via the props. prefix. Props are a plain object. They can hold scalars, arrays, objects, or any structure. Each item resolves props.* at render time, so a single props update can mean different things to different items.
// Declare props: scalars, objects, anything
pools: {
users: {
props: { showLabels: true, currency: '$' }
}
}
<!-- Reference props in template expressions -->
<div data-pool="users" data-key="id">
<template>
<div>
<span data-bind="name" data-show="props.showLabels"></span>
<span data-bind="props.currency + price"></span>
</div>
</template>
</div>
// Update props: one write, every item picks it up on the next flush
this.pools.users.props.currency = '€';
Props aren't limited to scalars. They can hold objects, arrays, or any structure, and items can index into them using their own properties. This lets a single prop mean different things to different items:
// Per-flock shape lookup: each item resolves its own shape
pool.props.shape = { 0: '', 1: 'shape-circle', 2: 'shape-diamond' };
// In the template: props.shape[flock] resolves per item
See it in action: The Boids demo uses props.shape as a per-flock lookup table. Select "Mixed" from the Shape dropdown to see 800 boids render with different shapes based on their flock color.
Things to Know
Item Order After Removal
Pools use O(1) swap-with-last removal for performance. When you call pool.remove(key), the removed item is swapped with the last item in the pool.items array before being removed. This means pool.items order is not guaranteed after removals.
The DOM order is unaffected. Elements stay in their insertion positions. But if you iterate pool.items and expect insertion order after removals, use pool.get(key) to access items by key instead of by array index.
If your use case requires guaranteed ordering, use data-list instead.
Template Scope
Pool templates can reference entity properties and props.* values. Unlike data-list, pool templates cannot access parent component state, stores, or computed properties directly. Use pool.props to bridge shared data into the template. This is the tradeoff that eliminates reactive proxy overhead.