Reactivity System
WildflowerJS uses a fine-grained, context-based reactivity system that provides surgical DOM updates without utilizing a virtual DOM.
- State is plain JavaScript objects.
- Mutations automatically trigger updates.
- Bindings track the properties they read.
- DOM updates are batched and precise.
Natural JavaScript mutations just work:
this.items.push(newItem)
this.user.name = "Jane"
Multiple changes in the same synchronous block are batched into a single microtask flush: three mutations, one DOM update.
Fine-Grained Reactivity
When you create bindings with data-bind, data-list, or other directives, WildflowerJS creates a context for each binding. These contexts form the unit of reactivity:
Virtual DOM Approach
- State changes
- Re-render entire component to virtual DOM
- Diff old vs new virtual DOM trees
- Patch real DOM with differences
Context-Based Approach (WildflowerJS)
- State changes
- Look up affected contexts by path
- Update only those specific DOM nodes
How It Works
Consider this component with multiple bindings:
<div data-component="user-profile">
<h1 data-bind="name"></h1> <!-- Context #1 -->
<p data-bind="bio"></p> <!-- Context #2 -->
<span data-bind="followers"></span> <!-- Context #3 -->
<span data-bind="following"></span> <!-- Context #4 -->
</div>
wildflower.component('user-profile', {
state: {
name: 'Jane',
bio: 'Developer',
followers: 1000,
following: 50
},
updateFollowers(count) {
// Only Context #3 updates - other DOM nodes untouched
this.followers = count
}
})
When followers changes:
- The framework looks up which contexts depend on the
followerspath - Only Context #3 (the followers span) is updated
- The
name,bio, andfollowingelements are not touched - No virtual DOM diffing or component re-render occurs
Context Hierarchy
Contexts form a hierarchy that mirrors your component structure:
Component Context (user-profile)
├── Binding Context (name)
├── Binding Context (bio)
├── List Context (posts)
│ ├── Item Context [0]
│ │ ├── Binding Context (posts[0].title)
│ │ └── Binding Context (posts[0].likes)
│ └── Item Context [1]
│ ├── Binding Context (posts[1].title)
│ └── Binding Context (posts[1].likes)
└── Conditional Context (showDetails)
This hierarchy enables:
- Scoped updates: Changing
posts[0].titleonly updates that specific binding - Efficient cleanup: Removing a list item cleans up all its child contexts
- Dependency tracking: Cross-component dependencies are tracked at the context level
Keyed List Rendering
WildflowerJS uses reactive list reconciliation to minimize DOM mutations. Each list item is tracked by identity, and per-item reactive effects ensure that only changed bindings update.
List Reconciliation
WildflowerJS uses a reactive mapArray approach for list rendering. When the array changes, the framework reconciles by identity, matching existing items by key, detecting adds, removes, and moves, and only touching the DOM for what actually changed:
| Change | What Happens |
|---|---|
| Append | New DOM elements created and appended; existing items untouched |
| Remove | Removed items' DOM elements destroyed; remaining items untouched |
| Reorder | DOM nodes moved to match new order; no re-creation |
| Property change | Per-item reactive effects detect the changed property and update only that binding |
Structural Changes
Adding, removing, and reordering items all go through identity-based reconciliation:
// Append: creates DOM only for new items
this.items.push({ id: 4, text: 'New item' })
// Remove: destroys only the removed item's DOM
this.items.splice(2, 1)
// Reorder: moves existing DOM nodes, no re-creation
const temp = this.rows[0]
this.rows[0] = this.rows[2]
this.rows[2] = temp
Per-Item Property Updates
When you change a property on an existing list item, only that item's affected bindings update; other items are untouched:
// Only the status binding on item 5 updates
this.items[5].status = 'completed'
// Each item's affected binding updates independently
this.items[2].name = 'Updated'
this.items[7].count = 42
<ul data-list="items">
<template>
<li>
<span data-bind="name"></span> <!-- Updated if name changed -->
<span data-bind="status"></span> <!-- Updated if status changed -->
<span data-bind="count"></span> <!-- Updated if count changed -->
</li>
</template>
</ul>
Automatic Keyed Rendering
WildflowerJS automatically enables keyed rendering when your list items have an id property. No additional configuration is required:
// Keyed rendering is automatic when items have 'id'
state: {
users: [
{ id: 1, name: 'Alice' }, // Tracked by id: 1
{ id: 2, name: 'Bob' } // Tracked by id: 2
]
}
// Without 'id', items are tracked by array index
state: {
tags: ['urgent', 'review', 'done'] // Tracked by index: 0, 1, 2
}
When items have an id property, the framework:
- Tracks each item by identity across array mutations
- Detects adds, removes, and reorders without re-creating DOM
- Preserves DOM state (focus, scroll position, form values) for unchanged items
- Runs per-item reactive effects that update only changed bindings
Why Keys Matter
Consider reordering a list. Without stable keys, the framework must assume items at each index changed:
// Before: ['Alice', 'Bob', 'Charlie']
// After: ['Charlie', 'Alice', 'Bob']
// Without keys: Updates ALL 3 items (index 0, 1, 2 all changed)
// With keys: Detects reorder, updates only positions
With keyed items, the framework recognizes that the same items exist in a different order and can optimize accordingly.
Batch Updates
Multiple state changes are automatically batched:
updateUser() {
// These are batched into a single render cycle
this.user.name = 'New Name'
this.user.email = 'new@email.com'
this.user.role = 'admin'
// DOM updated once, not three times
}
The framework uses microtask scheduling to batch updates that occur in the same JavaScript execution context.
Cross-Entity Automatic Dependency Tracking
WildflowerJS provides automatic dependency tracking when accessing stores, components, or plugins from within computed properties. The framework detects property access and registers dependencies automatically - no manual subscriptions needed.
Store Access: getStore()
Use wildflower.getStore() in computed properties for automatic store dependency tracking:
// Create a store
wildflower.store('cart', {
state: { items: [], total: 0 },
computed: {
itemCount() { return this.items.length; }
},
addItem(item) {
this.items.push(item);
this.total += item.price;
}
});
// Component automatically reacts to store changes
wildflower.component('cart-badge', {
computed: {
// Automatic dependency tracking - no subscribe() needed!
count() {
return wildflower.getStore('cart').items.length;
},
total() {
return wildflower.getStore('cart').total;
}
}
});
When cart.state.items changes, the count computed property automatically re-evaluates and updates the DOM.
subscribe block with this.stores shorthand is the recommended pattern. See Basic Stores for details.
Component Access: getComponent()
Use wildflower.getComponent() in computed properties to read from other components with automatic tracking:
// Source component with state
wildflower.component('theme-manager', {
state: { mode: 'light', primaryColor: '#007bff' }
});
// Observer component automatically tracks theme changes
wildflower.component('themed-panel', {
computed: {
themeClass() {
const theme = wildflower.getComponent('theme-manager');
return theme ? 'theme-' + theme.mode : 'theme-light';
},
panelStyle() {
const theme = wildflower.getComponent('theme-manager');
return theme ? { borderColor: theme.primaryColor } : {};
}
}
});
When theme-manager.state.mode changes, all components using getComponent('theme-manager') in their computed properties automatically update.
Plugin Access: $pluginName
Access plugins via wildflower['$pluginName'] with automatic dependency tracking:
// Register a plugin with reactive state
wildflower.plugin({
name: 'auth',
state: {
isLoggedIn: false,
user: null
},
login(user) {
this.isLoggedIn = true;
this.user = user;
},
logout() {
this.isLoggedIn = false;
this.user = null;
}
});
// Component automatically tracks plugin state
wildflower.component('user-menu', {
computed: {
showLoginButton() {
const auth = wildflower['$auth'];
return auth ? !auth.isLoggedIn : true;
},
userName() {
const auth = wildflower['$auth'];
return auth?.user?.name || 'Guest';
}
}
});
When the auth plugin's isLoggedIn state changes, all dependent computed properties re-evaluate automatically.
How Automatic Tracking Works
When a computed property is evaluated:
- The framework wraps entity access (
getStore,getComponent, plugin accessors) in tracking proxies - Property accesses on the returned entity are detected
- The component is registered as a dependent of those specific properties
- When those properties change, the computed property is re-evaluated
- Only the specific DOM bindings using that computed property update
init(), you may need to use subscribe() for reactive updates.
Preferred: $ Universal Entity Accessor
Access state from any entity directly in HTML templates. No computed wrappers needed:
<!-- Bind to another component's state -->
<span data-bind="$parent-component.count"></span>
<!-- Bind to a store's state or computed -->
<span data-bind="$my-store.value"></span>
<span data-bind="$my-store.computedTotal"></span>
<!-- Works in all data-* attributes -->
<div data-show="$auth.isLoggedIn">Welcome!</div>
<div data-list="$cart.items" data-key="id">...</div>
Performance Characteristics
Strengths
- Direct context lookup for updates
- No virtual DOM memory overhead
- Automatic array operation detection
- Zero-config swap and append optimization
- Minimal DOM mutations
Tradeoffs
- Memory for context registry
- Initial context creation overhead
- Garbage collection for context cleanup
Best Practices
For Lists
- Always include an
idproperty on list items to enable keyed rendering - Prefer
push()for appending items (triggers append optimization) - Mutate items in place when possible for sparse updates
- Avoid unnecessary array recreation when updating individual items
For General Reactivity
- Keep state flat when possible
- Batch related state changes together
- Use computed properties for derived state
- Leverage the automatic dependency tracking
// Good - mutation triggers sparse update
this.items[idx].completed = true
// Good - push triggers append optimization
this.items.push(newItem)
// Less optimal - full array replacement
this.items = this.items.map(item =>
item.id === id ? { ...item, completed: true } : item
)