Store API
Complete reference for WildflowerJS global stores.
Quick Reference
wildflower.store()
Creates a global reactive store that can be accessed from any component.
Signature
wildflower.store(name: string, definition?: StoreDefinition): Store
Parameters
| Parameter | Type | Description |
|---|---|---|
name |
string |
Unique identifier for the store |
definition |
StoreDefinition |
Store configuration (state, computed, methods, etc.). Optional; omit to retrieve an existing store. |
Returns
The store instance. If the store already exists, the existing instance is returned (the definition is ignored).
wildflower.store('cart') with one parameter returns the existing store, equivalent to wildflower.getStore('cart'). With two parameters, it creates the store if it doesn't exist.
Example
const userStore = wildflower.store('user', {
state: {
name: 'Guest',
email: '',
isLoggedIn: false,
preferences: {
theme: 'light',
notifications: true
}
},
computed: {
greeting() {
return this.isLoggedIn
? `Welcome back, ${this.name}!`
: 'Please sign in';
},
initials() {
if (!this.name) return '?';
return this.name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase();
}
},
// Methods at top level (same as components)
login(name, email) {
this.name = name;
this.email = email;
this.isLoggedIn = true;
},
logout() {
this.name = 'Guest';
this.email = '';
this.isLoggedIn = false;
},
setTheme(theme) {
this.preferences.theme = theme;
},
init() {
// Optional: Called when store is created
console.log('User store initialized');
// Load persisted state
const saved = localStorage.getItem('user');
if (saved) {
const data = JSON.parse(saved);
Object.assign(this.state, data);
}
}
});
Store Definition
Stores use the same definition pattern as components.
Structure
wildflower.store('store-name', {
// Reactive state
state: { /* ... */ },
// Derived values
computed: { /* ... */ },
// Type validation (dev builds)
types: { /* ... */ },
// State change watchers
watch: { /* ... */ },
// localStorage persistence
storageKey: 'my-store', // Enables localStorage persistence
autoSave: true, // Auto-save state on every change
// Called when store is created
init() { /* ... */ },
// Called when store is destroyed
destroy() { /* ... */ },
// Animation/polling loop (called each frame with delta time)
tick(dt) { /* ... */ },
// Custom methods - at top level
methodName() { /* ... */ },
anotherMethod() { /* ... */ }
});
Definition Keys
| Key | Type | Description |
|---|---|---|
state |
object |
Reactive state properties. Accessed via proxy (e.g., store.count not store.state.count). |
computed |
object |
Derived values that auto-update when dependencies change. |
types |
object |
Type validation for state properties (dev builds only). |
watch |
object |
State change watchers. Same syntax as components: watch: { propName(newVal, oldVal) { ... } } |
storageKey |
string |
Enables localStorage persistence for store state. The state is saved/loaded using this key. |
autoSave |
boolean |
When true, automatically saves state to localStorage on every change. Requires storageKey to be set. |
init() |
function |
Lifecycle hook called when the store is created. Use for setup, loading persisted data, etc. |
destroy() |
function |
Lifecycle cleanup hook called when the store is destroyed. Use for clearing intervals, releasing resources, etc. |
tick(dt) |
function |
Animation/polling loop method, called each animation frame with delta time in seconds. Enables headless animation patterns where the store drives updates via requestAnimationFrame. |
| methods | function |
Custom methods defined at the top level of the definition (not inside an actions block). |
Key Differences from Components
| Feature | Components | Stores |
|---|---|---|
| DOM Element | Has this.element |
No element (DOM-less) |
| Props | Receives props from parent | No props |
| Lifecycle | init, destroy, onUpdate, tick |
init, destroy, tick |
| Scope | Instance per DOM element | Singleton - one instance globally |
Complete Example
wildflower.store('cart', {
state: {
items: [],
discount: 0,
currency: 'USD'
},
types: {
discount: 'number',
currency: 'string',
items: 'array'
},
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
);
},
total() {
const sub = this.subtotal;
return sub - (sub * this.discount / 100);
},
isEmpty() {
return this.items.length === 0;
}
},
watch: {
items() {
// Persist cart to localStorage
localStorage.setItem('cart', JSON.stringify(this.items));
}
},
// Methods
addItem(product, quantity = 1) {
const existing = this.items.find(i => i.id === product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.items.push({ ...product, quantity });
}
},
removeItem(productId) {
const index = this.items.findIndex(i => i.id === productId);
if (index > -1) {
this.items.splice(index, 1);
}
},
updateQuantity(productId, quantity) {
const item = this.items.find(i => i.id === productId);
if (item) {
item.quantity = quantity;
}
},
clear() {
this.items = [];
this.discount = 0;
},
applyDiscount(percent) {
this.discount = Math.min(100, Math.max(0, percent));
},
init() {
// Restore cart from localStorage
const saved = localStorage.getItem('cart');
if (saved) {
this.items = JSON.parse(saved);
}
}
});
wildflower.getStore()
Retrieves a store instance by name. When used in computed properties, automatically tracks dependencies.
Signature
wildflower.getStore(name: string): Store | null
Parameters
| Parameter | Type | Description |
|---|---|---|
name |
string |
Name of the store to retrieve |
Returns
The store instance, or null if not found.
getStore() is called inside a computed property, the framework automatically detects store property access and registers the component as a dependent. The computed property will re-evaluate when the accessed store data changes.
Example
// Get the store
const userStore = wildflower.getStore('user');
// Access state via proxy
console.log(userStore.name);
console.log(userStore.isLoggedIn);
// Access computed (also via proxy)
console.log(userStore.greeting);
// Call methods
userStore.login('Alice', 'alice@example.com');
userStore.logout();
// Modify state via proxy (triggers reactivity)
userStore.preferences.theme = 'dark';
In Computed Properties (Automatically Reactive)
wildflower.component('stats-display', {
computed: {
// Automatically reactive - no manual subscription needed!
totalItems() {
return wildflower.getStore('cart').items.length;
},
// Access store computed properties
cartTotal() {
return wildflower.getStore('cart').total;
},
// Access nested properties
userName() {
return wildflower.getStore('auth').user?.name || 'Guest';
}
}
});
In Component Methods
wildflower.component('login-form', {
state: { email: '', password: '' },
async handleSubmit() {
const response = await this.authenticate();
if (response.success) {
// Update global store
const userStore = wildflower.getStore('user');
userStore.login(response.user.name, response.user.email);
}
},
async authenticate() {
// API call...
}
});
$storeName.path (Template Shorthand)
Access store data directly in HTML templates using the $ prefix. Works in all data-* attributes.
Syntax
$storeName.path
Examples
<!-- Bind to store state -->
<span data-bind="$user.name"></span>
<!-- Conditional visibility -->
<div data-show="$auth.isLoggedIn">Welcome back!</div>
<!-- Store-backed lists -->
<div data-list="$cart.items" data-key="id">
<template>
<div data-bind="name"></div>
</template>
</div>
<!-- Nested paths -->
<span data-bind="$auth.user.address.city"></span>
<!-- In expressions -->
<div data-bind-class="$auth.isLoggedIn ? 'online' : 'offline'"></div>
<div data-show="$cart.items.length > 0">Cart is not empty</div>
subscribe: {} (Component Block)
Declaratively subscribe to store changes in your component definition. Enables this.stores auto-injection and onStoreUpdate() callbacks.
Signature
subscribe: {
[storeName: string]: string[] // Array of paths to subscribe to
}
Description
Add a subscribe block to your component definition to:
- Enable
this.stores- automatic store injection - Receive
onStoreUpdate()callbacks when subscribed paths change - Automatically clean up subscriptions when component is destroyed
Example
wildflower.component('cart-summary', {
state: { itemCount: 0, total: 0 },
// Subscribe to cart.items and cart.discount
subscribe: {
cart: ['items', 'discount'],
user: ['preferences']
},
init() {
// this.stores.cart and this.stores.user are now available
this.updateFromStore();
},
onStoreUpdate(storeName, path, newValue, oldValue) {
if (storeName === 'cart') {
this.updateFromStore();
}
},
updateFromStore() {
this.itemCount = this.stores.cart.items.length;
this.total = this.stores.cart.total;
}
});
user.profile.name, subscribe to 'profile' - you'll receive the entire profile object when any nested property changes.
this.stores
Auto-injected object containing references to stores declared in the subscribe block.
Type
this.stores: {
[storeName: string]: Store // Store instances
}
Availability
Available in:
init()and all lifecycle hooks- All component methods
- Computed properties
Example
wildflower.component('user-menu', {
subscribe: {
auth: ['user', 'token'],
cart: ['items']
},
computed: {
userName() {
return this.stores.auth.user?.name || 'Guest';
},
cartCount() {
return this.stores.cart.itemCount;
}
},
logout() {
this.stores.auth.logout();
},
addToCart(product) {
this.stores.cart.addItem(product);
}
});
Accessing Store Properties
// Access state
this.stores.auth.user
this.stores.cart.items
// Access computed properties
this.stores.auth.isAuthenticated
this.stores.cart.total
// Call store methods
this.stores.auth.login(email, password)
this.stores.cart.clear()
this.stores is only available when you declare a subscribe block. Without it, use wildflower.getStore() instead.
onStoreUpdate()
Lifecycle hook called when a subscribed store path changes.
Signature
onStoreUpdate(
storeName: string,
path: string,
newValue: any,
oldValue: any
): void
Parameters
| Parameter | Type | Description |
|---|---|---|
storeName |
string |
Name of the store that changed |
path |
string |
The subscribed path that changed |
newValue |
any |
The new value |
oldValue |
any |
The previous value |
Example
wildflower.component('order-status', {
state: {
status: 'pending',
lastUpdate: null
},
subscribe: {
order: ['status', 'items'],
notifications: ['messages']
},
onStoreUpdate(storeName, path, newValue, oldValue) {
console.log(`${storeName}.${path} changed:`, oldValue, '→', newValue);
if (storeName === 'order' && path === 'status') {
this.status = newValue;
this.lastUpdate = new Date();
// Show notification on status change
if (newValue === 'shipped') {
this.showShippingNotification();
}
}
if (storeName === 'notifications' && path === 'messages') {
// New messages arrived
const newCount = newValue.length - (oldValue?.length || 0);
if (newCount > 0) {
this.playNotificationSound();
}
}
},
showShippingNotification() { /* ... */ },
playNotificationSound() { /* ... */ }
});
onStoreUpdate() for side effects (sounds, notifications, analytics). For simple state synchronization, computed properties that read from this.stores are often cleaner.
store.subscribe()
Subscribe to changes on a specific store property.
Signature
store.subscribe(
path: string,
callback: (newValue: any, oldValue: any) => void,
options?: { immediate?: boolean, once?: boolean }
): () => void
Parameters
| Parameter | Type | Description |
|---|---|---|
path |
string |
Property path to watch |
callback |
function |
Called when the value changes |
options |
object |
Optional. immediate: true fires the callback immediately with the current value. once: true automatically unsubscribes after the first change. |
Returns
An unsubscribe function. Call it to stop receiving updates.
Example
const userStore = wildflower.getStore('user');
// Subscribe to a property
const unsubscribe = userStore.subscribe('isLoggedIn', (isLoggedIn, wasLoggedIn) => {
if (isLoggedIn && !wasLoggedIn) {
console.log('User just logged in!');
analytics.track('login');
} else if (!isLoggedIn && wasLoggedIn) {
console.log('User just logged out');
analytics.track('logout');
}
});
// Later: stop listening
unsubscribe();
In Component Lifecycle
wildflower.component('notifications', {
state: { messages: [] },
init() {
const store = wildflower.getStore('notification-store');
// Store unsubscribe for cleanup
this.unsubscribe = store.subscribe('messages', (messages) => {
this.messages = messages;
});
},
destroy() {
// Clean up subscription
if (this.unsubscribe) {
this.unsubscribe();
}
}
});
Multiple Subscriptions
wildflower.component('dashboard', {
subscriptions: [],
init() {
const authStore = wildflower.getStore('auth');
const settingsStore = wildflower.getStore('settings');
// Track all subscriptions
this.subscriptions.push(
authStore.subscribe('user', this.handleUserChange.bind(this)),
settingsStore.subscribe('theme', this.handleThemeChange.bind(this)),
settingsStore.subscribe('language', this.handleLanguageChange.bind(this))
);
},
destroy() {
// Unsubscribe from all
this.subscriptions.forEach(unsub => unsub());
},
handleUserChange(user) { /* ... */ },
handleThemeChange(theme) { /* ... */ },
handleLanguageChange(lang) { /* ... */ }
});
Store Methods
Custom methods defined in the store for encapsulating business logic.
Defining Methods
wildflower.store('todos', {
state: {
items: [],
filter: 'all' // 'all' | 'active' | 'completed'
},
computed: {
filteredItems() {
switch (this.filter) {
case 'active':
return this.items.filter(t => !t.done);
case 'completed':
return this.items.filter(t => t.done);
default:
return this.items;
}
},
activeCount() {
return this.items.filter(t => !t.done).length;
}
},
// Methods at top level
addTodo(text) {
this.items.push({
id: Date.now(),
text,
done: false,
createdAt: new Date()
});
},
removeTodo(id) {
const index = this.items.findIndex(t => t.id === id);
if (index > -1) {
this.items.splice(index, 1);
}
},
toggleTodo(id) {
const todo = this.items.find(t => t.id === id);
if (todo) {
todo.done = !todo.done;
}
},
setFilter(filter) {
this.filter = filter;
},
clearCompleted() {
this.items = this.items.filter(t => !t.done);
},
// Async methods work too
async syncWithServer() {
const response = await fetch('/api/todos');
const serverTodos = await response.json();
this.items = serverTodos;
}
});
Calling Store Methods
// From anywhere
const todoStore = wildflower.getStore('todos');
todoStore.addTodo('Learn WildflowerJS');
todoStore.toggleTodo(123);
todoStore.setFilter('active');
// From component methods
wildflower.component('add-todo-form', {
state: { text: '' },
submit() {
if (this.text.trim()) {
wildflower.getStore('todos').addTodo(this.text);
this.text = '';
}
}
});
Common Patterns
Authentication Store
wildflower.store('auth', {
state: {
user: null,
token: null,
isLoading: false,
error: null
},
computed: {
isAuthenticated() {
return !!this.token && !!this.user;
}
},
async login(email, password) {
this.isLoading = true;
this.error = null;
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) throw new Error('Login failed');
const data = await response.json();
this.user = data.user;
this.token = data.token;
localStorage.setItem('token', data.token);
} catch (error) {
this.error = error.message;
} finally {
this.isLoading = false;
}
},
logout() {
this.user = null;
this.token = null;
localStorage.removeItem('token');
},
init() {
const token = localStorage.getItem('token');
if (token) {
this.token = token;
this.fetchUser();
}
},
async fetchUser() {
if (!this.token) return;
try {
const response = await fetch('/api/me', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
this.user = await response.json();
} catch {
this.logout();
}
}
});
Theme Store
wildflower.store('theme', {
state: {
mode: 'light', // 'light' | 'dark' | 'system'
accentColor: '#007bff'
},
computed: {
effectiveMode() {
if (this.mode === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
return this.mode;
},
isDark() {
return this.effectiveMode === 'dark';
}
},
watch: {
mode(newMode) {
localStorage.setItem('theme-mode', newMode);
}
},
setMode(mode) {
this.mode = mode;
document.documentElement.setAttribute('data-theme', this.effectiveMode);
},
toggle() {
this.setMode(this.isDark ? 'light' : 'dark');
},
init() {
const saved = localStorage.getItem('theme-mode');
if (saved) {
this.mode = saved;
}
document.documentElement.setAttribute('data-theme', this.effectiveMode);
}
});