Stores & Events
The framework provides two mechanisms for cross-component and cross-module communication: Stores for shared state and Events for notifications.
Stores
Section titled “Stores”Stores hold shared state that multiple components can access and react to.
Creating a Store
Section titled “Creating a Store”const { registry, bus } = window.fullfinity;
registry.store('cart', { items: [], total: 0,
add(product, quantity = 1) { const existing = this.items.find(i => i.productId === product.id); if (existing) { existing.quantity += quantity; } else { this.items.push({ productId: product.id, name: product.name, price: product.price, quantity }); } this.recalculate(); bus.emit('cart:updated', { items: this.items }); },
remove(productId) { this.items = this.items.filter(i => i.productId !== productId); this.recalculate(); bus.emit('cart:updated', { items: this.items }); },
recalculate() { this.total = this.items.reduce( (sum, item) => sum + (item.price * item.quantity), 0 ); },
clear() { this.items = []; this.total = 0; bus.emit('cart:cleared'); }});Accessing Stores in Components
Section titled “Accessing Stores in Components”Use the useStore hook to access a store and re-render when it changes:
registry.component('CartIcon', function() { const { useStore, html, bus, useEffect } = window.fullfinity; const cart = useStore('cart');
// Re-render when cart updates useEffect(() => { const unsubscribe = bus.on('cart:updated', () => { // useStore already handles this, but you can add extra logic }); return unsubscribe; }, []);
const itemCount = cart?.items?.length || 0;
return html` <a href="/cart" class="cart-icon"> <svg>...</svg> ${itemCount > 0 && html` <span class="badge">${itemCount}</span> `} </a> `;});
registry.component('CartTotal', function() { const { useStore, html } = window.fullfinity; const cart = useStore('cart');
return html` <div class="cart-total"> Total: $${(cart?.total || 0).toFixed(2)} </div> `;});Notifying Store Updates
Section titled “Notifying Store Updates”When you modify a store, notify components to re-render:
registry.store('user', { profile: null, preferences: {},
async fetchProfile() { const response = await fetch('/api/user/profile'); this.profile = await response.json(); // Notify components using this store fullfinity.notifyStore('user'); },
updatePreference(key, value) { this.preferences[key] = value; fullfinity.notifyStore('user'); }});Store Patterns
Section titled “Store Patterns”Async Store Initialization
Section titled “Async Store Initialization”registry.store('products', { items: [], loading: true, error: null,
async init() { try { const response = await fetch('/api/products'); this.items = await response.json(); this.loading = false; } catch (e) { this.error = e.message; this.loading = false; } fullfinity.notifyStore('products'); }});
// Initialize on page loaddocument.addEventListener('DOMContentLoaded', () => { fullfinity.registry.store('products').init();});Computed Values in Stores
Section titled “Computed Values in Stores”registry.store('cart', { items: [],
get itemCount() { return this.items.reduce((sum, item) => sum + item.quantity, 0); },
get total() { return this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0); },
get isEmpty() { return this.items.length === 0; }});Persisted Stores
Section titled “Persisted Stores”registry.store('cart', { items: JSON.parse(localStorage.getItem('cart') || '[]'),
save() { localStorage.setItem('cart', JSON.stringify(this.items)); },
add(product, quantity) { // ... add logic this.save(); fullfinity.notifyStore('cart'); }});Event Bus
Section titled “Event Bus”The event bus enables pub/sub communication between components and modules.
Basic Usage
Section titled “Basic Usage”const { bus } = window.fullfinity;
// Subscribe to an eventconst unsubscribe = bus.on('user:logged-in', (data) => { console.log('User logged in:', data.userId);});
// Emit an eventbus.emit('user:logged-in', { userId: 123, name: 'John' });
// Unsubscribeunsubscribe();Events in Components
Section titled “Events in Components”registry.component('NotificationBell', function() { const { useState, useEffect, html, bus } = window.fullfinity; const [notifications, setNotifications] = useState([]);
useEffect(() => { // Subscribe to notification events const unsubscribe = bus.on('notification:new', (notification) => { setNotifications(prev => [...prev, notification]); });
// Cleanup on unmount return unsubscribe; }, []);
return html` <div class="notification-bell"> <svg>...</svg> ${notifications.length > 0 && html` <span class="badge">${notifications.length}</span> `} </div> `;});One-Time Events
Section titled “One-Time Events”Use once for events you only want to handle once:
bus.once('app:initialized', () => { console.log('App initialized - this only logs once');});Event Naming Conventions
Section titled “Event Naming Conventions”Use namespaced event names to avoid collisions:
module:actionmodule:entity:actionExamples:
cart:updatedcart:item:addedcart:item:removeduser:logged-inuser:logged-outcheckout:startedcheckout:completedloyalty:points-earned
Cross-Module Communication
Section titled “Cross-Module Communication”Events are perfect for module coordination:
// ecommerce modulebus.emit('order:placed', { orderId: 123, total: 99.99 });
// ecommerce_loyalty module (listens)bus.on('order:placed', async ({ orderId, total }) => { const points = Math.floor(total); await fetch('/api/loyalty/add-points', { method: 'POST', body: JSON.stringify({ orderId, points }) }); bus.emit('loyalty:points-earned', { points });});
// ecommerce_analytics module (also listens)bus.on('order:placed', ({ orderId, total }) => { analytics.track('purchase', { orderId, value: total });});Event Patterns
Section titled “Event Patterns”Request/Response Pattern
Section titled “Request/Response Pattern”// Component that needs databus.emit('product:request', { id: 123 });bus.once('product:response:123', (product) => { setProduct(product);});
// Product service respondsbus.on('product:request', async ({ id }) => { const product = await fetchProduct(id); bus.emit(`product:response:${id}`, product);});Command Pattern
Section titled “Command Pattern”// UI sends commandsbus.emit('cart:command:add', { productId: 123, quantity: 2 });
// Cart module handles commandsbus.on('cart:command:add', ({ productId, quantity }) => { const cart = registry.store('cart'); cart.add(productId, quantity);});Combining Stores and Events
Section titled “Combining Stores and Events”Best practice: Use stores for state, events for notifications:
registry.store('cart', { items: [],
add(product, quantity) { this.items.push({ ...product, quantity });
// Notify store subscribers (re-renders components) fullfinity.notifyStore('cart');
// Also emit event (for analytics, other modules, etc.) bus.emit('cart:item:added', { product, quantity }); }});
// Analytics modulebus.on('cart:item:added', ({ product, quantity }) => { analytics.track('add_to_cart', { product_id: product.id, quantity, value: product.price * quantity });});
// Inventory modulebus.on('cart:item:added', ({ product, quantity }) => { // Reserve inventory api.reserveStock(product.id, quantity);});Store vs Event: When to Use Which
Section titled “Store vs Event: When to Use Which”| Use Case | Store | Event |
|---|---|---|
| Shared state | ✅ | ❌ |
| Component re-renders on change | ✅ | ⚠️ Manual |
| One-way notifications | ❌ | ✅ |
| Cross-module communication | ❌ | ✅ |
| Analytics/logging | ❌ | ✅ |
| Persistent data | ✅ | ❌ |
Store API Reference
Section titled “Store API Reference”const { registry, notifyStore } = window.fullfinity;
// Register a storeregistry.store('name', { /* state and methods */ });
// Get a storeconst cart = registry.store('cart');
// Check if store existsregistry.hasStore('cart'); // true/false
// List all storesregistry.listStores(); // ['cart', 'user', ...]
// Notify components that store updatednotifyStore('cart');Event Bus API Reference
Section titled “Event Bus API Reference”const { bus } = window.fullfinity;
// Subscribe to eventconst unsubscribe = bus.on('event:name', (data) => { ... });
// Unsubscribeunsubscribe();// orbus.off('event:name', handlerFunction);
// Emit eventbus.emit('event:name', { /* data */ });
// Subscribe oncebus.once('event:name', (data) => { ... });Best Practices
Section titled “Best Practices”1. Keep Stores Simple
Section titled “1. Keep Stores Simple”// Good: Simple, focused storeregistry.store('cart', { items: [], add(item) { ... }, remove(id) { ... }});
// Bad: Store doing too muchregistry.store('everything', { cart: [], user: null, products: [], orders: [], // ... becomes unmaintainable});2. Always Clean Up Event Subscriptions
Section titled “2. Always Clean Up Event Subscriptions”useEffect(() => { const unsub1 = bus.on('event1', handler1); const unsub2 = bus.on('event2', handler2);
return () => { unsub1(); unsub2(); };}, []);3. Document Store Shape
Section titled “3. Document Store Shape”/** * Cart Store * * Shape: * { * items: Array<{productId, name, price, quantity}>, * total: number, * shipping: number | null, * discount: { code: string, amount: number } | null * } * * Events emitted: * - cart:updated - When items change * - cart:cleared - When cart is emptied */registry.store('cart', { ... });4. Don’t Overuse Events
Section titled “4. Don’t Overuse Events”// Bad: Event for every little thingbus.emit('button:hovered');bus.emit('input:focused');bus.emit('input:character-typed');
// Good: Events for significant actionsbus.emit('form:submitted', data);bus.emit('order:placed', order);