Advanced Store Patterns
Deep dive into advanced store techniques including store-to-store communication, persistence, lifecycle management, and comprehensive API reference.
Store-to-Store Communication
Stores can communicate with each other for complex state management, enabling sophisticated workflows and real-time updates:
// Shopping Cart Store - Central e-commerce state management
wildflower.store('cart', {
state: {
items: [],
discount: 0,
promoCode: '',
shipping: 0,
tax: 0,
taxRate: 0.08
},
computed: {
itemCount() {
return this.items.reduce((sum, item) => sum + item.quantity, 0)
},
subtotal() {
return this.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
)
},
// Formatted versions for display
subtotalFormatted() {
return this.subtotal.toFixed(2)
},
taxFormatted() {
return this.tax.toFixed(2)
},
total() {
return Math.max(0, this.subtotal - this.discount + this.shipping + this.tax).toFixed(2)
},
isEmpty() {
return this.items.length === 0
}
},
// Methods (unified paradigm - at top level)
addItem(product) {
const existingItem = this.items.find(item => item.productId === product.id)
if (existingItem) {
existingItem.quantity += 1
} else {
this.items.push({
id: Date.now(),
productId: product.id,
name: product.name,
price: product.price,
quantity: 1,
addedAt: new Date()
})
}
// Notify analytics store
const analyticsStore = wildflower.getStore('analytics')
analyticsStore.trackEvent('item_added', {
productId: product.id,
productName: product.name,
price: product.price,
cartTotal: this.subtotal
})
// Check for promotional discounts
this.updatePromotions()
},
removeItem(itemId) {
const item = this.items.find(item => item.id === itemId)
if (item) {
// Track removal
const analyticsStore = wildflower.getStore('analytics')
analyticsStore.trackEvent('item_removed', {
productId: item.productId,
productName: item.name,
price: item.price,
quantity: item.quantity
})
// Remove from cart
const index = this.items.findIndex(item => item.id === itemId)
this.items.splice(index, 1)
// Recalculate promotions
this.updatePromotions()
}
},
updateQuantity(itemId, quantity) {
const item = this.items.find(item => item.id === itemId)
if (item && quantity > 0) {
item.quantity = quantity
this.updatePromotions()
}
},
applyPromoCode(code) {
this.promoCode = code
this.updatePromotions()
},
updatePromotions() {
const promoStore = wildflower.getStore('promotions')
const discount = promoStore.calculateDiscount(this.items, this.promoCode)
this.discount = discount
// Calculate shipping
this.shipping = this.subtotal >= 50 ? 0 : 5.99
// Calculate tax
this.tax = (this.subtotal - this.discount) * this.taxRate
},
clearCart() {
const analyticsStore = wildflower.getStore('analytics')
analyticsStore.trackEvent('cart_cleared', {
itemCount: this.items.length,
totalValue: this.subtotal
})
this.items = []
this.discount = 0
this.promoCode = ''
this.shipping = 0
this.tax = 0
}
})
// Analytics Store - Track user behavior and generate insights
wildflower.store('analytics', {
state: {
events: [],
sessionId: null,
startTime: null,
pageViews: 0,
conversionFunnel: {
views: 0,
addToCart: 0,
checkout: 0,
purchase: 0
}
},
computed: {
sessionDuration() {
return Math.round((new Date() - this.startTime) / 1000)
},
conversionRate() {
return this.conversionFunnel.views > 0
? Math.round((this.conversionFunnel.addToCart / this.conversionFunnel.views) * 100)
: 0
},
recentEvents() {
return this.events.slice(-10).reverse()
}
},
init() {
this.sessionId = 'session_' + Date.now()
this.startTime = new Date()
this.trackEvent('session_start', {
timestamp: this.startTime,
userAgent: navigator.userAgent
})
},
// Methods (unified paradigm - at top level)
trackEvent(eventName, data) {
const event = {
id: Date.now(),
event: eventName,
data: data,
formattedTime: new Date().toLocaleTimeString(),
sessionId: this.sessionId
}
this.events.push(event)
// Update conversion funnel
this.updateConversionFunnel(eventName, data)
// Send to analytics service (simulated)
this.sendToAnalytics(event)
// Keep events list manageable
if (this.events.length > 100) {
this.events = this.events.slice(-50)
}
},
updateConversionFunnel(eventName, data) {
switch (eventName) {
case 'item_added':
this.conversionFunnel.addToCart++
break
case 'checkout_started':
this.conversionFunnel.checkout++
break
case 'purchase_completed':
this.conversionFunnel.purchase++
break
}
},
trackPageView(page) {
this.pageViews++
this.conversionFunnel.views++
this.trackEvent('page_view', {
page: page,
totalViews: this.pageViews
})
},
async sendToAnalytics(event) {
try {
// Simulate API call
console.log('📊 Analytics Event:', event)
// In a real app, you'd send to your analytics service
// await fetch('/api/analytics', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(event)
// })
} catch (error) {
console.error('Analytics error:', error)
}
},
clearEvents() {
this.events = []
}
})
// Promotions Store - Handle discounts and special offers
wildflower.store('promotions', {
state: {
availablePromotions: [
{ code: 'SAVE10', discount: 0.10, minAmount: 25, description: '10% off orders over $25' },
{ code: 'WELCOME', discount: 5, minAmount: 0, description: '$5 off first order' },
{ code: 'BULK20', discount: 0.20, minAmount: 100, description: '20% off orders over $100' }
],
activePromotions: [],
seasonalMultiplier: 1.0
},
computed: {
activePromoCodes() {
return this.availablePromotions.map(p => p.code)
}
},
// Methods at top level (Unified Entity Paradigm)
calculateDiscount(cartItems, promoCode) {
const subtotal = cartItems.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
)
let discount = 0
// Apply promo code discount
if (promoCode) {
const promo = this.availablePromotions.find(p => p.code === promoCode)
if (promo && subtotal >= promo.minAmount) {
if (promo.discount < 1) {
// Percentage discount
discount += subtotal * promo.discount
} else {
// Fixed amount discount
discount += promo.discount
}
}
}
// Apply automatic quantity discounts
const itemCount = cartItems.reduce((sum, item) => sum + item.quantity, 0)
if (itemCount >= 5) {
discount += subtotal * 0.05 // 5% bulk discount
}
// Apply seasonal multiplier
discount *= this.seasonalMultiplier
// Track promotion usage
if (discount > 0) {
const analyticsStore = wildflower.getStore('analytics')
analyticsStore.trackEvent('promotion_applied', {
promoCode: promoCode,
discount: discount,
subtotal: subtotal,
itemCount: itemCount
})
}
return Math.round(discount * 100) / 100
},
validatePromoCode(code) {
const promo = this.availablePromotions.find(p => p.code === code)
return promo ? promo : null
},
addPromotion(promotion) {
this.availablePromotions.push(promotion)
},
setSeasonalMultiplier(multiplier) {
this.seasonalMultiplier = multiplier
// Notify about seasonal changes
const analyticsStore = wildflower.getStore('analytics')
analyticsStore.trackEvent('seasonal_promotion_updated', {
multiplier: multiplier
})
}
})
<div data-component="store-communication-demo">
<p class="text-muted">Demonstrates how stores communicate and share data in real-time.</p>
<div class="row">
<div class="col-md-6">
<!-- Shopping Cart -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between">
<h6 class="mb-0">Shopping Cart</h6>
<span class="badge bg-primary" data-bind="$cart.itemCount">0</span>
</div>
<div class="card-body">
<!-- Product Selection -->
<div class="mb-3">
<h6>Add Products:</h6>
<div class="d-grid gap-2">
<button data-action="addWidget" class="btn btn-primary btn-sm">
Add Widget ($19.99)
</button>
<button data-action="addGadget" class="btn btn-primary btn-sm">
Add Gadget ($29.99)
</button>
<button data-action="addTool" class="btn btn-primary btn-sm">
Add Tool ($39.99)
</button>
</div>
</div>
<!-- Cart Items -->
<div data-show="!$cart.isEmpty">
<div class="mb-3">
<h6>Cart Items:</h6>
<div data-list="$cart.items">
<template>
<div class="d-flex justify-content-between align-items-center py-2 border-bottom">
<div>
<strong data-bind="name"></strong>
<br>
<small class="text-muted">
$<span data-bind="price"></span> x <span data-bind="quantity"></span>
</small>
</div>
<button data-action="removeFromCart" class="btn btn-sm btn-danger">
Remove
</button>
</div>
</template>
</div>
</div>
<!-- Promo Code -->
<div class="mb-3">
<label class="form-label">Promo Code:</label>
<div class="d-flex gap-2">
<input type="text" data-model="promoCode" class="form-control"
placeholder="Enter code (try SAVE10)">
<button data-action="applyPromo" class="btn btn-secondary">Apply</button>
</div>
<small class="text-muted">Try: SAVE10, WELCOME, BULK20</small>
</div>
<!-- Cart Summary -->
<div class="border-top pt-3">
<div class="d-flex justify-content-between">
<span>Subtotal:</span>
<span>$<span data-bind="$cart.subtotalFormatted"></span></span>
</div>
<div class="d-flex justify-content-between" data-show="$cart.discount > 0">
<span class="text-success">Discount:</span>
<span class="text-success">-$<span data-bind="$cart.discount"></span></span>
</div>
<div class="d-flex justify-content-between">
<span>Shipping:</span>
<span>$<span data-bind="$cart.shipping"></span></span>
</div>
<div class="d-flex justify-content-between">
<span>Tax:</span>
<span>$<span data-bind="$cart.taxFormatted"></span></span>
</div>
<div class="d-flex justify-content-between fw-bold border-top pt-2">
<span>Total:</span>
<span>$<span data-bind="$cart.total"></span></span>
</div>
</div>
<button data-action="clearCart" class="btn btn-danger mt-3 w-100">
Clear Cart
</button>
</div>
<div data-show="$cart.isEmpty" class="text-center text-muted">
Cart is empty
</div>
</div>
</div>
</div>
<div class="col-md-6">
<!-- Analytics Dashboard -->
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0">Analytics Dashboard</h6>
</div>
<div class="card-body">
<div class="row text-center mb-3">
<div class="col-6">
<div class="fw-bold" data-bind="$analytics.sessionDuration">0</div>
<small class="text-muted">Session (seconds)</small>
</div>
<div class="col-6">
<div class="fw-bold" data-bind="$analytics.conversionRate">0</div>
<small class="text-muted">Conversion %</small>
</div>
</div>
<div class="mb-3">
<h6>Conversion Funnel:</h6>
<div class="small">
<div>Views: <span data-bind="$analytics.conversionFunnel.views"></span></div>
<div>Add to Cart: <span data-bind="$analytics.conversionFunnel.addToCart"></span></div>
<div>Checkout: <span data-bind="$analytics.conversionFunnel.checkout"></span></div>
<div>Purchase: <span data-bind="$analytics.conversionFunnel.purchase"></span></div>
</div>
</div>
<div>
<h6>Recent Events:</h6>
<div style="max-height: 200px; overflow-y: auto;">
<div data-list="$analytics.recentEvents">
<template>
<div class="small border-bottom py-1">
<div class="d-flex justify-content-between gap-2">
<span data-bind="event" class="fw-bold"></span>
<span data-bind="formattedTime" class="text-muted text-nowrap"></span>
</div>
</div>
</template>
</div>
</div>
</div>
<button data-action="clearAnalytics" class="btn btn-secondary btn-sm mt-3">
Clear Events
</button>
</div>
</div>
</div>
</div>
</div>
wildflower.component('store-communication-demo', {
state: {
promoCode: ''
},
// Subscribe to stores for this.stores auto-injection
subscribe: {
cart: ['items', 'discount'],
analytics: ['events']
},
init() {
// Initialize page view tracking using this.stores
this.stores.analytics.trackPageView('store-communication-demo')
},
// Product addition actions
addWidget() {
this.stores.cart.addItem({
id: 'widget',
name: 'Widget',
price: 19.99
})
},
addGadget() {
this.stores.cart.addItem({
id: 'gadget',
name: 'Gadget',
price: 29.99
})
},
addTool() {
this.stores.cart.addItem({
id: 'tool',
name: 'Tool',
price: 39.99
})
},
// Cart management actions
removeFromCart(event, element, details) {
const itemId = details.item.id
this.stores.cart.removeItem(itemId)
},
applyPromo() {
this.stores.cart.applyPromoCode(this.promoCode.toUpperCase())
this.promoCode = '' // Clear input
},
clearCart() {
this.stores.cart.clearCart()
},
// Analytics actions
clearAnalytics() {
this.stores.analytics.clearEvents()
}
})
Built-in Persistence (storageKey & autoSave)
WildflowerJS provides built-in localStorage persistence for stores. Simply add storageKey and autoSave options to your store definition:
// Automatic persistence - no manual localStorage code needed!
wildflower.store('app-settings', {
storageKey: 'my-app-settings', // localStorage key
autoSave: true, // Auto-save on ANY state change
state: {
theme: 'light',
fontSize: 'medium',
notifications: true,
recentItems: []
},
setTheme(theme) {
this.theme = theme;
// Automatically saved to localStorage!
},
addRecentItem(item) {
this.recentItems = [...this.recentItems, item].slice(-10);
// Nested changes are also auto-saved!
}
});
// On page reload, state is automatically restored from localStorage
storageKey- The localStorage key where state will be savedautoSave: true- Automatically saves state on ANY change, including nested properties- State is automatically restored from localStorage when the store is created
- Works with all state types: primitives, arrays, nested objects
When to use built-in persistence vs manual:
- Built-in - Simple use cases where you want the entire store state persisted automatically
- Manual - When you need selective persistence, encryption, or complex storage logic (see below)
Manual Store Persistence
For more control, implement custom persistence with lifecycle hooks. This demo saves your name and theme to localStorage. Change them, then refresh the page to see them persist:
// Preferences store: loads from localStorage on init, saves on change
wildflower.store('prefs', {
state: { name: 'Guest', theme: 'light' },
init() {
const saved = localStorage.getItem('prefs');
if (saved) {
try {
const data = JSON.parse(saved);
if (data.name) this.name = data.name;
if (data.theme) this.theme = data.theme;
} catch (e) { /* ignore bad data */ }
}
},
setName(name) {
this.name = name;
this._save();
},
setTheme(theme) {
this.theme = theme;
this._save();
},
_save() {
localStorage.setItem('prefs', JSON.stringify({
name: this.name, theme: this.theme
}));
},
clearAll() {
this.name = 'Guest';
this.theme = 'light';
localStorage.removeItem('prefs');
}
});
Components bind to the store with $prefs.name and $prefs.theme. Changes persist across page reloads. The store's init() restores from localStorage on startup.
Store Lifecycle
Stores support full component lifecycle methods for initialization and cleanup, enabling sophisticated background operations and resource management:
// Data Service Store - Advanced lifecycle management with connections and retries
wildflower.store('data-service', {
state: {
connected: false,
connecting: false,
data: null,
error: null,
retryCount: 0,
maxRetries: 5,
connectionHistory: [],
lastSync: null,
syncStatus: 'idle', // idle, syncing, success, error
connectionQuality: 100, // 0-100 percentage
bandwidth: 'high', // low, medium, high
latency: 0
},
computed: {
connectionStatus() {
if (this.connecting) return 'connecting'
if (this.connected) return 'connected'
if (this.error) return 'error'
return 'disconnected'
},
qualityLevel() {
if (this.connectionQuality >= 80) return 'excellent'
if (this.connectionQuality >= 60) return 'good'
if (this.connectionQuality >= 40) return 'fair'
return 'poor'
},
recentHistory() {
return this.connectionHistory.slice(0, 10)
},
dataAge() {
if (!this.lastSync) return 0
return Math.round((new Date() - this.lastSync) / 1000)
}
},
// Initialize when store is created
init() {
console.log('🚀 Data Service Store initializing...')
this.initializeConnectionHistory()
this.connect()
this.setupPeriodicSync()
this.setupConnectionMonitoring()
},
// Cleanup when store is destroyed
destroy() {
console.log('🛑 Data Service Store destroying...')
if (this.syncInterval) {
clearInterval(this.syncInterval)
}
if (this.monitorInterval) {
clearInterval(this.monitorInterval)
}
this.disconnect()
this.logConnection('destroyed', 'Store destroyed')
},
// Helper methods
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
// Methods at top level (Unified Entity Paradigm)
initializeConnectionHistory() {
this.connectionHistory = []
this.logConnection('initialization', 'Store initialized')
},
async connect() {
if (this.connecting || this.connected) return
try {
this.connecting = true
this.error = null
this.logConnection('connecting', 'Attempting connection...')
// Simulate connection negotiation
await this.delay(Math.random() * 1000 + 500)
// Simulate occasional connection failures
if (Math.random() < 0.15) {
throw new Error('Connection timeout')
}
this.connected = true
this.connecting = false
this.retryCount = 0
this.connectionQuality = Math.round(Math.random() * 30 + 70)
this.bandwidth = ['low', 'medium', 'high'][Math.floor(Math.random() * 3)]
this.latency = Math.round(Math.random() * 100 + 20)
this.logConnection('connected', 'Connection established successfully')
this.loadInitialData()
} catch (error) {
this.connected = false
this.connecting = false
this.error = error.message
this.retryCount++
this.logConnection('error', `Connection failed: ${error.message}`)
// Retry with exponential backoff
if (this.retryCount < this.maxRetries) {
const delay = Math.pow(2, this.retryCount) * 1000
this.logConnection('retry', `Retrying in ${delay}ms (attempt ${this.retryCount + 1})`)
setTimeout(() => this.connect(), delay)
} else {
this.logConnection('failed', 'Max retries exceeded')
}
}
},
async loadInitialData() {
if (!this.connected) return
try {
this.syncStatus = 'syncing'
this.logConnection('sync', 'Loading data...')
// Simulate data loading with variable delay
await this.delay(Math.random() * 1000 + 300)
// Simulate occasional data errors
if (Math.random() < 0.1) {
throw new Error('Data fetch failed')
}
this.data = {
lastSync: new Date(),
items: this.generateSampleData(),
version: Math.floor(Math.random() * 1000),
checksum: this.generateChecksum()
}
this.lastSync = new Date()
this.syncStatus = 'success'
this.logConnection('sync_success', `Data loaded: ${this.data.items.length} items`)
} catch (error) {
this.error = error.message
this.syncStatus = 'error'
this.logConnection('sync_error', `Data load failed: ${error.message}`)
}
},
async refreshData() {
if (!this.connected) {
this.connect()
return
}
await this.loadInitialData()
},
setupPeriodicSync() {
this.syncInterval = setInterval(() => {
if (this.connected && this.syncStatus !== 'syncing') {
this.loadInitialData()
}
}, 15000) // Sync every 15 seconds
},
setupConnectionMonitoring() {
this.monitorInterval = setInterval(() => {
if (this.connected) {
// Simulate connection quality changes
const qualityChange = Math.random() * 20 - 10
this.connectionQuality = Math.round(Math.max(0, Math.min(100,
this.connectionQuality + qualityChange)))
// Simulate latency changes
this.latency = Math.round(Math.max(10,
this.latency + Math.random() * 40 - 20))
// Simulate disconnection on poor quality
if (this.connectionQuality < 20) {
this.disconnect()
}
}
}, 5000) // Monitor every 5 seconds
},
disconnect() {
if (this.connected) {
this.connected = false
this.connecting = false
this.connectionQuality = 0
this.logConnection('disconnected', 'Connection closed')
// Auto-reconnect after a delay
setTimeout(() => {
if (!this.connected && !this.connecting) {
this.connect()
}
}, 3000)
}
},
forceReconnect() {
this.disconnect()
setTimeout(() => this.connect(), 1000)
},
clearHistory() {
this.connectionHistory = []
this.logConnection('history_cleared', 'Connection history cleared')
},
logConnection(type, message) {
const entry = {
id: Date.now(),
type,
message,
timestamp: new Date().toLocaleTimeString(),
quality: this.connectionQuality,
latency: this.latency
}
this.connectionHistory.unshift(entry)
// Keep history manageable
if (this.connectionHistory.length > 50) {
this.connectionHistory = this.connectionHistory.slice(0, 50)
}
},
generateSampleData() {
const items = []
const count = Math.floor(Math.random() * 10) + 5
for (let i = 0; i < count; i++) {
items.push({
id: i + 1,
name: `Item ${i + 1}`,
value: Math.floor(Math.random() * 1000),
type: ['A', 'B', 'C'][Math.floor(Math.random() * 3)]
})
}
return items
},
generateChecksum() {
return Math.random().toString(36).substring(7).toUpperCase()
}
})
// Background Tasks Store - Task scheduling and lifecycle management
wildflower.store('background-tasks', {
state: {
tasks: [],
runningTasks: 0,
maxConcurrentTasks: 3,
totalCompleted: 0,
totalFailed: 0,
scheduler: null,
paused: false
},
computed: {
pendingTasks() {
return this.tasks.filter(t => t.status === 'pending')
},
runningTaskList() {
return this.tasks.filter(t => t.status === 'running')
},
completedTasks() {
return this.tasks.filter(t => t.status === 'completed')
},
failedTasks() {
return this.tasks.filter(t => t.status === 'failed')
},
successRate() {
const total = this.totalCompleted + this.totalFailed
return total > 0 ? Math.round((this.totalCompleted / total) * 100) : 0
}
},
init() {
console.log('⏰ Background Tasks Store initializing...')
this.startScheduler()
this.addSampleTasks()
},
destroy() {
console.log('🛑 Background Tasks Store destroying...')
if (this.scheduler) {
clearInterval(this.scheduler)
}
// Cancel all pending tasks
this.tasks.forEach(task => {
if (task.status === 'pending') {
task.status = 'cancelled'
}
})
},
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
// Methods at top level (Unified Entity Paradigm)
addTask(task) {
const newTask = {
id: Date.now() + Math.random(),
name: task.name,
type: task.type || 'generic',
priority: task.priority || 'normal',
status: 'pending',
progress: 0,
duration: task.duration || 3000,
createdAt: new Date(),
startedAt: null,
completedAt: null,
error: null,
result: null
}
this.tasks.push(newTask)
this.sortTasksByPriority()
},
addSampleTasks() {
const sampleTasks = [
{ name: 'Data Backup', type: 'backup', priority: 'high', duration: 5000 },
{ name: 'Cache Cleanup', type: 'maintenance', priority: 'normal', duration: 2000 },
{ name: 'Report Generation', type: 'report', priority: 'low', duration: 8000 },
{ name: 'Log Rotation', type: 'maintenance', priority: 'normal', duration: 1500 },
{ name: 'Security Scan', type: 'security', priority: 'high', duration: 7000 }
]
sampleTasks.forEach(task => this.addTask(task))
},
sortTasksByPriority() {
const priorityOrder = { 'high': 0, 'normal': 1, 'low': 2 }
this.tasks.sort((a, b) => {
if (a.status === 'pending' && b.status !== 'pending') return -1
if (a.status !== 'pending' && b.status === 'pending') return 1
return priorityOrder[a.priority] - priorityOrder[b.priority]
})
},
async runTask(taskId) {
const task = this.tasks.find(t => t.id === taskId)
if (!task || task.status !== 'pending') return
task.status = 'running'
task.startedAt = new Date()
task.progress = 0
this.runningTasks++
try {
// Simulate task execution with progress updates
const steps = 10
for (let i = 0; i < steps; i++) {
await this.delay(task.duration / steps)
task.progress = Math.round(((i + 1) / steps) * 100)
// Simulate occasional task failures
if (Math.random() < 0.1) {
throw new Error('Task execution failed')
}
}
task.status = 'completed'
task.completedAt = new Date()
task.result = `Task completed successfully in ${task.duration}ms`
this.totalCompleted++
} catch (error) {
task.status = 'failed'
task.error = error.message
task.completedAt = new Date()
this.totalFailed++
}
this.runningTasks--
},
startScheduler() {
if (this.scheduler) return
this.scheduler = setInterval(() => {
if (this.paused) return
// Find pending tasks that can be started
const pendingTasks = this.tasks.filter(t => t.status === 'pending')
const availableSlots = this.maxConcurrentTasks - this.runningTasks
if (pendingTasks.length > 0 && availableSlots > 0) {
const tasksToStart = pendingTasks.slice(0, availableSlots)
tasksToStart.forEach(task => this.runTask(task.id))
}
}, 1000)
},
pauseScheduler() {
this.paused = true
},
resumeScheduler() {
this.paused = false
},
cancelTask(taskId) {
const task = this.tasks.find(t => t.id === taskId)
if (task && task.status === 'pending') {
task.status = 'cancelled'
task.completedAt = new Date()
}
},
clearCompleted() {
this.tasks = this.tasks.filter(t =>
!['completed', 'failed', 'cancelled'].includes(t.status)
)
},
addRandomTask() {
const types = ['backup', 'maintenance', 'report', 'security', 'sync']
const priorities = ['high', 'normal', 'low']
this.addTask({
name: `Random Task ${Math.floor(Math.random() * 1000)}`,
type: types[Math.floor(Math.random() * types.length)],
priority: priorities[Math.floor(Math.random() * priorities.length)],
duration: Math.floor(Math.random() * 5000) + 1000
})
},
resetStats() {
this.totalCompleted = 0
this.totalFailed = 0
}
})
<div data-component="lifecycle-demo">
<p class="text-muted">Demonstrates advanced store lifecycle management with background tasks and data services.</p>
<div class="row">
<div class="col-md-6">
<!-- Data Service Status -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">Data Service</h6>
<span data-bind="$data-service.connectionStatus"
data-bind-class="$data-service.connected ? 'badge bg-success' : $data-service.connecting ? 'badge bg-warning' : 'badge bg-danger'">
disconnected
</span>
</div>
<div class="card-body">
<!-- Connection Info -->
<div class="row mb-3">
<div class="col-4">
<small class="text-muted">Quality</small>
<div class="fw-bold">
<span data-bind="$data-service.connectionQuality">0</span>%
<span data-bind="$data-service.qualityLevel"
data-bind-class="$data-service.qualityLevel === 'excellent' ? 'text-success' : $data-service.qualityLevel === 'good' ? 'text-info' : $data-service.qualityLevel === 'fair' ? 'text-warning' : 'text-danger'">
(poor)
</span>
</div>
</div>
<div class="col-4">
<small class="text-muted">Latency</small>
<div class="fw-bold"><span data-bind="$data-service.latency">0</span>ms</div>
</div>
<div class="col-4">
<small class="text-muted">Bandwidth</small>
<div class="fw-bold text-capitalize" data-bind="$data-service.bandwidth">high</div>
</div>
</div>
<!-- Data Info -->
<div data-show="$data-service.data">
<div class="mb-3">
<h6>Data Status:</h6>
<div class="row">
<div class="col-6">
<small class="text-muted">Items</small>
<div class="fw-bold"><span data-bind="$data-service.data ? $data-service.data.items.length : 0">0</span></div>
</div>
<div class="col-6">
<small class="text-muted">Age</small>
<div class="fw-bold"><span data-bind="$data-service.dataAge">0</span>s</div>
</div>
</div>
</div>
</div>
<!-- Error Display -->
<div data-show="$data-service.error" class="alert alert-danger py-2">
<small>Error: <span data-bind="$data-service.error"></span></small>
</div>
<!-- Actions -->
<div class="d-grid gap-2">
<button data-action="refreshData" class="btn btn-primary btn-sm">
Refresh Data
</button>
<button data-action="forceReconnect" class="btn btn-warning btn-sm">
Force Reconnect
</button>
<button data-action="clearServiceHistory" class="btn btn-secondary btn-sm">
Clear History
</button>
</div>
</div>
</div>
<!-- Connection History -->
<div class="card">
<div class="card-header">
<h6 class="mb-0">Connection History</h6>
</div>
<div class="card-body p-0">
<div style="max-height: 200px; overflow-y: auto;">
<div data-list="$data-service.recentHistory">
<template>
<div class="px-3 py-2 border-bottom small">
<div class="d-flex justify-content-between">
<span data-bind="type" class="fw-bold text-capitalize"></span>
<span data-bind="timestamp" class="text-muted"></span>
</div>
<div data-bind="message" class="text-muted"></div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<!-- Background Tasks -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">Background Tasks</h6>
<span data-bind="$background-tasks.runningTasks" class="badge bg-info">0</span>
</div>
<div class="card-body">
<!-- Task Stats -->
<div class="row mb-3">
<div class="col-4">
<small class="text-muted">Pending</small>
<div class="fw-bold"><span data-bind="$background-tasks.pendingTasks.length">0</span></div>
</div>
<div class="col-4">
<small class="text-muted">Success Rate</small>
<div class="fw-bold"><span data-bind="$background-tasks.successRate">0</span>%</div>
</div>
<div class="col-4">
<small class="text-muted">Failed</small>
<div class="fw-bold text-danger"><span data-bind="$background-tasks.totalFailed">0</span></div>
</div>
</div>
<!-- Scheduler Controls -->
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>Scheduler:</span>
<span data-bind="$background-tasks.paused ? 'PAUSED' : 'RUNNING'"
data-bind-class="$background-tasks.paused ? 'text-warning' : 'text-success'">
RUNNING
</span>
</div>
<button data-action="pauseScheduler" class="btn btn-warning btn-sm me-2">
Pause
</button>
<button data-action="resumeScheduler" class="btn btn-success btn-sm">
Resume
</button>
</div>
<!-- Task Actions -->
<div class="d-grid gap-2">
<button data-action="addRandomTask" class="btn btn-primary btn-sm">
Add Random Task
</button>
<button data-action="clearCompleted" class="btn btn-secondary btn-sm">
Clear Completed
</button>
<button data-action="resetTaskStats" class="btn btn-danger btn-sm">
Reset Stats
</button>
</div>
</div>
</div>
<!-- Running Tasks -->
<div class="card">
<div class="card-header">
<h6 class="mb-0">Active Tasks</h6>
</div>
<div class="card-body p-0">
<div style="max-height: 200px; overflow-y: auto;">
<div data-list="$background-tasks.runningTaskList">
<template>
<div class="px-3 py-2 border-bottom">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong data-bind="name"></strong>
<br>
<small class="text-muted">
<span data-bind="type" class="text-capitalize"></span> •
<span data-bind="priority" class="text-capitalize"></span>
</small>
</div>
<div class="text-end">
<div data-bind="progress" class="fw-bold">0</div>
<small class="text-muted">%</small>
</div>
</div>
<div class="progress mt-2" style="height: 12px;">
<div class="progress-bar bg-primary"
data-bind-style="{ width: progress + '%' }">
</div>
</div>
</div>
</template>
</div>
<div data-show="$background-tasks.runningTaskList.length === 0" class="text-center p-3 text-muted">
No active tasks
</div>
</div>
</div>
</div>
</div>
</div>
</div>
wildflower.component('lifecycle-demo', {
// Data Service actions
refreshData() {
const dataService = wildflower.getStore('data-service')
dataService.refreshData()
},
forceReconnect() {
const dataService = wildflower.getStore('data-service')
dataService.forceReconnect()
},
clearServiceHistory() {
const dataService = wildflower.getStore('data-service')
dataService.clearHistory()
},
// Background Tasks actions
addRandomTask() {
const taskStore = wildflower.getStore('background-tasks')
taskStore.addRandomTask()
},
pauseScheduler() {
const taskStore = wildflower.getStore('background-tasks')
taskStore.pauseScheduler()
},
resumeScheduler() {
const taskStore = wildflower.getStore('background-tasks')
taskStore.resumeScheduler()
},
clearCompleted() {
const taskStore = wildflower.getStore('background-tasks')
taskStore.clearCompleted()
},
resetTaskStats() {
const taskStore = wildflower.getStore('background-tasks')
taskStore.resetStats()
}
})
Headless Simulations: tick() in a Store
Stores can implement tick(dt) to drive per-frame work that lives outside any component. The store owns the simulation state and advances it each animation frame; one or more components subscribe and render. Since the store outlives any view, you can switch routes, swap viewports, or render the same simulation in multiple components, and the simulation keeps running.
See the physarum-store demo for a complete example: a physics store with no DOM owns the agent grid and pheromone field, and a separate viewport component subscribes and renders. tick(dt) signature, dt clamping, and ordering rules are documented on the Lifecycle Hooks page.
Virtual Component Protection
Store components are automatically protected from garbage collection during DOM changes:
isVirtual: true flag that protects them from being destroyed during modal operations, route changes, or other DOM manipulations. This ensures persistent global state.
// Internal store component structure
const storeInstance = {
id: instanceId,
name: storeName,
state: reactiveState,
stateManager: stateManager,
definition: storeDefinition,
context: storeContext,
isVirtual: true // This flag protects from DOM-based cleanup
}
// Framework garbage collection excludes virtual components
if (instance.isVirtual || instance.isPersistent) {
// Don't garbage collect virtual/persistent components
return
}
Store API Quick Reference
| API | Purpose |
|---|---|
wildflower.store(name, config) | Create a global store |
wildflower.getStore(name) | Get store instance |
$storeName.property | Bind to store state in HTML |
subscribe: ['storeName'] | Wait for store + enable this.stores |
subscribe: { store: ['path'] } | Wait + receive onStoreUpdate notifications |
store.subscribe(path, callback) | Programmatic subscription |
watch: { 'store:name.path': fn } | Watch store changes in component |
$ shorthand, onStoreUpdate, and subscribe-wait timing.
Store Initialization and Readiness
Stores with async init() provide readiness APIs. Components using subscribe: automatically wait for stores before their init() runs. See Store API Reference for isReady(), waitForReady(), and timeout configuration.
Store-Backed List Patterns
When rendering lists from store data, you have three options. All three work correctly with full reactivity:
Option 1: $ Shorthand (Recommended)
The most direct approach - bind the list directly to store data:
<!-- Bind directly to store items -->
<div data-list="$cart.items">
<template>
<div class="item">
<span data-bind="name"></span>
<span data-bind="price"></span>
</div>
</template>
</div>
<!-- Bind to a computed store property -->
<div data-list="$inventory.availableItems">
<template>...</template>
</div>
Option 2: Computed Property
Use a computed property that returns store data:
<div data-list="items">
<template>
<div class="item" data-bind="name"></div>
</template>
</div>
wildflower.component('item-list', {
state: {},
computed: {
items() {
return wildflower.getStore('myStore').items
}
}
})
Option 3: Subscribe Pattern
Sync store data to component state using subscriptions:
wildflower.component('synced-list', {
state: {
items: []
},
init() {
const store = wildflower.getStore('myStore')
// Initial sync
this.items = [...store.items]
// Subscribe to changes - callback receives (newValue, oldValue)
store.subscribe('items', (newItems) => {
if (Array.isArray(newItems)) {
this.items = [...newItems]
}
})
}
})
- $entity.path - Simplest for read-only bindings, no extra component code needed
- computed - Clean when you need to derive values from store data
- subscribe - Required for two-way sync when component has editable local state that mirrors store data
$entity.path syntax only provides one-way display binding.
Example: A settings panel inside a list item component that edits column names. When the store updates (from another component), the input field values need to update too.
Best Practices
✅ Do
- Create focused, single-responsibility stores
- Use computed properties for derived data
- Implement store persistence for user settings
- Use store-to-store communication for complex workflows
- Leverage lifecycle hooks for initialization and cleanup
- Trust the framework's virtual component protection
❌ Don't
- Create monolithic stores mixing unrelated data
- Mutate store state directly from components
- Forget to handle async operations properly
- Create circular dependencies between stores
- Store temporary UI state in global stores
- Manually manage store cleanup (framework handles it)