Skip to content

Stores & Events

The framework provides two mechanisms for cross-component and cross-module communication: Stores for shared state and Events for notifications.

Stores hold shared state that multiple components can access and react to.

ecommerce/static/src/js/components.js
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');
}
});

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>
`;
});

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');
}
});
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 load
document.addEventListener('DOMContentLoaded', () => {
fullfinity.registry.store('products').init();
});
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;
}
});
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');
}
});

The event bus enables pub/sub communication between components and modules.

const { bus } = window.fullfinity;
// Subscribe to an event
const unsubscribe = bus.on('user:logged-in', (data) => {
console.log('User logged in:', data.userId);
});
// Emit an event
bus.emit('user:logged-in', { userId: 123, name: 'John' });
// Unsubscribe
unsubscribe();
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>
`;
});

Use once for events you only want to handle once:

bus.once('app:initialized', () => {
console.log('App initialized - this only logs once');
});

Use namespaced event names to avoid collisions:

module:action
module:entity:action

Examples:

  • cart:updated
  • cart:item:added
  • cart:item:removed
  • user:logged-in
  • user:logged-out
  • checkout:started
  • checkout:completed
  • loyalty:points-earned

Events are perfect for module coordination:

// ecommerce module
bus.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 });
});
// Component that needs data
bus.emit('product:request', { id: 123 });
bus.once('product:response:123', (product) => {
setProduct(product);
});
// Product service responds
bus.on('product:request', async ({ id }) => {
const product = await fetchProduct(id);
bus.emit(`product:response:${id}`, product);
});
// UI sends commands
bus.emit('cart:command:add', { productId: 123, quantity: 2 });
// Cart module handles commands
bus.on('cart:command:add', ({ productId, quantity }) => {
const cart = registry.store('cart');
cart.add(productId, quantity);
});

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 module
bus.on('cart:item:added', ({ product, quantity }) => {
analytics.track('add_to_cart', {
product_id: product.id,
quantity,
value: product.price * quantity
});
});
// Inventory module
bus.on('cart:item:added', ({ product, quantity }) => {
// Reserve inventory
api.reserveStock(product.id, quantity);
});
Use CaseStoreEvent
Shared state
Component re-renders on change⚠️ Manual
One-way notifications
Cross-module communication
Analytics/logging
Persistent data
const { registry, notifyStore } = window.fullfinity;
// Register a store
registry.store('name', { /* state and methods */ });
// Get a store
const cart = registry.store('cart');
// Check if store exists
registry.hasStore('cart'); // true/false
// List all stores
registry.listStores(); // ['cart', 'user', ...]
// Notify components that store updated
notifyStore('cart');
const { bus } = window.fullfinity;
// Subscribe to event
const unsubscribe = bus.on('event:name', (data) => { ... });
// Unsubscribe
unsubscribe();
// or
bus.off('event:name', handlerFunction);
// Emit event
bus.emit('event:name', { /* data */ });
// Subscribe once
bus.once('event:name', (data) => { ... });
// Good: Simple, focused store
registry.store('cart', {
items: [],
add(item) { ... },
remove(id) { ... }
});
// Bad: Store doing too much
registry.store('everything', {
cart: [],
user: null,
products: [],
orders: [],
// ... becomes unmaintainable
});
useEffect(() => {
const unsub1 = bus.on('event1', handler1);
const unsub2 = bus.on('event2', handler2);
return () => {
unsub1();
unsub2();
};
}, []);
/**
* 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', { ... });
// Bad: Event for every little thing
bus.emit('button:hovered');
bus.emit('input:focused');
bus.emit('input:character-typed');
// Good: Events for significant actions
bus.emit('form:submitted', data);
bus.emit('order:placed', order);