Shopping Cart Example
Learn how to use multiple stores with useInitSync for domain separation and independent data loading.
This example demonstrates the domain-separated stores pattern using useInitSync() with multiple independent stores. Each domain (products, cart, user) has its own store and initialization logic.
🚀 Live Interactive Playground
Experience Mesa's multi-store architecture with our comprehensive shopping cart playground:
The playground demonstrates:
- Multi-store architecture with independent domain stores
- Parallel data initialization without blocking UI
- Real shopping cart functionality with add/remove/update operations
- User authentication flow with conditional cart loading
- Product catalog filtering and category management
- Performance monitoring showing independent store updates
Features Demonstrated
- 🏪 Multiple Independent Stores: Separate stores for different domains
- 🚀 Parallel Data Loading: Independent initialization without blocking
- 🎯 Domain Separation: Clear separation of concerns
- 🔄 Independent Updates: Each store updates independently
- 🛒 Complex State Management: Shopping cart functionality with proper architecture
Store Architecture
import { proxy } from "mesa-react";
// Products domain - manages product catalog
const productsStore = proxy({
products: [],
loading: true,
error: null,
categories: [],
});
// Shopping cart domain - manages cart state
const cartStore = proxy({
items: [],
total: 0,
itemCount: 0,
loading: false,
});
// User domain - manages user session
const userStore = proxy({
user: null,
isAuthenticated: false,
preferences: {
currency: 'USD',
theme: 'light',
},
loading: true,
});
Complete Implementation
import React, { useState } from "react";
import { proxy, useStore, useInitSync } from "mesa-react";
// Store definitions (as shown above)
const productsStore = proxy({
products: [],
loading: true,
error: null,
categories: [],
});
const cartStore = proxy({
items: [],
total: 0,
itemCount: 0,
loading: false,
});
const userStore = proxy({
user: null,
isAuthenticated: false,
preferences: {
currency: 'USD',
theme: 'light',
},
loading: true,
});
// Mock API functions
const fetchProducts = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{
id: 1,
name: "Wireless Headphones",
price: 99.99,
image: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=200&h=200&fit=crop",
category: "Electronics",
description: "High-quality wireless headphones with noise cancellation.",
stock: 15,
},
{
id: 2,
name: "Smart Watch",
price: 299.99,
image: "https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=200&h=200&fit=crop",
category: "Electronics",
description: "Feature-rich smartwatch with health tracking.",
stock: 8,
},
{
id: 3,
name: "Coffee Mug",
price: 14.99,
image: "https://images.unsplash.com/photo-1514228742587-6b1558fcf93a?w=200&h=200&fit=crop",
category: "Home",
description: "Beautiful ceramic coffee mug for your morning brew.",
stock: 25,
},
{
id: 4,
name: "Notebook Set",
price: 24.99,
image: "https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?w=200&h=200&fit=crop",
category: "Stationery",
description: "Premium notebook set for all your writing needs.",
stock: 12,
},
]);
}, 800);
});
};
const fetchUserSession = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
user: {
id: 1,
name: "Alex Smith",
email: "alex@example.com",
avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=40&h=40&fit=crop&crop=face",
},
isAuthenticated: true,
preferences: {
currency: 'USD',
theme: 'light',
}
});
}, 500);
});
};
const fetchCart = (userId) => {
return new Promise((resolve) => {
setTimeout(() => {
// Simulate existing cart items
resolve([
{ productId: 1, quantity: 1 },
{ productId: 3, quantity: 2 },
]);
}, 600);
});
};
// Main Shopping App Component
function ShoppingApp() {
// Initialize all stores independently
useProductsInitialization();
useUserInitialization();
useCartInitialization();
return (
<div className="max-w-6xl mx-auto p-6">
<Header />
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div className="lg:col-span-3">
<ProductCatalog />
</div>
<div>
<CartSidebar />
</div>
</div>
</div>
);
}
// Products store initialization
function useProductsInitialization() {
useInitSync(productsStore, async (state) => {
state.loading = true;
state.error = null;
try {
const products = await fetchProducts();
const categories = [...new Set(products.map(p => p.category))];
state.products = products;
state.categories = categories;
} catch (error) {
state.error = error.message;
} finally {
state.loading = false;
}
});
}
// User store initialization
function useUserInitialization() {
useInitSync(userStore, async (state) => {
state.loading = true;
try {
const sessionData = await fetchUserSession();
state.user = sessionData.user;
state.isAuthenticated = sessionData.isAuthenticated;
state.preferences = sessionData.preferences;
} catch (error) {
state.isAuthenticated = false;
state.user = null;
} finally {
state.loading = false;
}
});
}
// Cart store initialization
function useCartInitialization() {
const { user, isAuthenticated } = useStore(userStore, s => ({
user: s.user,
isAuthenticated: s.isAuthenticated
}));
useInitSync(cartStore, async (state) => {
if (!isAuthenticated || !user) {
// Empty cart for non-authenticated users
state.items = [];
state.total = 0;
state.itemCount = 0;
return;
}
state.loading = true;
try {
const cartItems = await fetchCart(user.id);
state.items = cartItems;
calculateCartTotals(state);
} catch (error) {
console.error("Failed to load cart:", error);
state.items = [];
} finally {
state.loading = false;
}
}, {
deps: [user?.id, isAuthenticated] // Re-run when user changes
});
}
// Header Component
function Header() {
const { user, isAuthenticated, loading } = useStore(userStore);
const { itemCount } = useStore(cartStore, s => ({ itemCount: s.itemCount }));
return (
<header className="flex justify-between items-center mb-8 pb-4 border-b">
<h1 className="text-3xl font-bold text-gray-900">Mesa Store</h1>
<div className="flex items-center space-x-4">
<CartIcon itemCount={itemCount} />
{loading ? (
<div className="w-8 h-8 bg-gray-300 rounded-full animate-pulse"></div>
) : isAuthenticated && user ? (
<UserMenu user={user} />
) : (
<LoginButton />
)}
</div>
</header>
);
}
// Product Catalog Component
function ProductCatalog() {
const { products, loading, error, categories } = useStore(productsStore);
const [selectedCategory, setSelectedCategory] = useState('All');
if (loading) {
return <ProductLoadingSkeleton />;
}
if (error) {
return (
<div className="text-center py-12">
<p className="text-red-600">Failed to load products: {error}</p>
</div>
);
}
const filteredProducts = selectedCategory === 'All'
? products
: products.filter(p => p.category === selectedCategory);
return (
<div>
<CategoryFilter
categories={['All', ...categories]}
selected={selectedCategory}
onSelect={setSelectedCategory}
/>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{filteredProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
// Product Card Component
function ProductCard({ product }) {
const [isAdding, setIsAdding] = useState(false);
const handleAddToCart = async () => {
setIsAdding(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 300));
// Add to cart store
const existingItem = cartStore.items.find(item => item.productId === product.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
cartStore.items.push({ productId: product.id, quantity: 1 });
}
calculateCartTotals(cartStore);
setIsAdding(false);
};
return (
<div className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
<img
src={product.image}
alt={product.name}
className="w-full h-32 object-cover rounded-md mb-3"
/>
<h3 className="font-semibold text-gray-900">{product.name}</h3>
<p className="text-sm text-gray-600 mb-2">{product.description}</p>
<div className="flex justify-between items-center">
<span className="text-lg font-bold text-green-600">
${product.price.toFixed(2)}
</span>
<button
onClick={handleAddToCart}
disabled={isAdding || product.stock === 0}
className={`px-3 py-1 rounded text-sm font-medium ${
isAdding
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: product.stock === 0
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
} transition-colors`}
>
{isAdding ? 'Adding...' : product.stock === 0 ? 'Out of Stock' : 'Add to Cart'}
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
{product.stock} in stock
</p>
</div>
);
}
// Cart Sidebar Component
function CartSidebar() {
const { items, total, itemCount, loading } = useStore(cartStore);
const products = useStore(productsStore, s => s.products);
if (loading) {
return (
<div className="bg-gray-50 p-4 rounded-lg">
<div className="animate-pulse space-y-3">
<div className="h-4 bg-gray-300 rounded w-3/4"></div>
<div className="h-3 bg-gray-300 rounded w-1/2"></div>
</div>
</div>
);
}
if (items.length === 0) {
return (
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-semibold mb-3">Shopping Cart</h3>
<p className="text-gray-600 text-sm">Your cart is empty</p>
</div>
);
}
return (
<div className="bg-gray-50 p-4 rounded-lg sticky top-6">
<h3 className="font-semibold mb-3">
Shopping Cart ({itemCount} items)
</h3>
<div className="space-y-2 mb-4">
{items.map(item => {
const product = products.find(p => p.id === item.productId);
if (!product) return null;
return (
<CartItem
key={item.productId}
item={item}
product={product}
/>
);
})}
</div>
<div className="border-t pt-3">
<div className="flex justify-between items-center text-lg font-bold">
<span>Total:</span>
<span className="text-green-600">${total.toFixed(2)}</span>
</div>
<button className="w-full mt-3 bg-green-600 text-white py-2 rounded hover:bg-green-700 transition-colors">
Checkout
</button>
</div>
</div>
);
}
// Cart Item Component
function CartItem({ item, product }) {
const updateQuantity = (newQuantity) => {
if (newQuantity === 0) {
const index = cartStore.items.findIndex(i => i.productId === item.productId);
cartStore.items.splice(index, 1);
} else {
item.quantity = newQuantity;
}
calculateCartTotals(cartStore);
};
return (
<div className="flex items-center space-x-3 py-2">
<img
src={product.image}
alt={product.name}
className="w-10 h-10 object-cover rounded"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{product.name}</p>
<p className="text-xs text-gray-600">${product.price.toFixed(2)}</p>
</div>
<div className="flex items-center space-x-1">
<button
onClick={() => updateQuantity(item.quantity - 1)}
className="w-6 h-6 rounded bg-gray-200 flex items-center justify-center text-sm"
>
-
</button>
<span className="w-8 text-center text-sm">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.quantity + 1)}
className="w-6 h-6 rounded bg-gray-200 flex items-center justify-center text-sm"
>
+
</button>
</div>
</div>
);
}
// Helper Components
function ProductLoadingSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="border border-gray-200 rounded-lg p-4 animate-pulse">
<div className="w-full h-32 bg-gray-300 rounded-md mb-3"></div>
<div className="h-4 bg-gray-300 rounded mb-2"></div>
<div className="h-3 bg-gray-300 rounded mb-4 w-3/4"></div>
<div className="flex justify-between items-center">
<div className="h-5 bg-gray-300 rounded w-16"></div>
<div className="h-8 bg-gray-300 rounded w-20"></div>
</div>
</div>
))}
</div>
);
}
function CategoryFilter({ categories, selected, onSelect }) {
return (
<div className="mb-6">
<div className="flex flex-wrap gap-2">
{categories.map(category => (
<button
key={category}
onClick={() => onSelect(category)}
className={`px-3 py-1 rounded-full text-sm font-medium ${
selected === category
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
} transition-colors`}
>
{category}
</button>
))}
</div>
</div>
);
}
function CartIcon({ itemCount }) {
return (
<div className="relative">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4m0 0L7 13m0 0l-1.5 6M7 13l-1.5-6M17 13v6a2 2 0 01-2 2H9a2 2 0 01-2-2v-6"/>
</svg>
{itemCount > 0 && (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{itemCount > 99 ? '99+' : itemCount}
</span>
)}
</div>
);
}
function UserMenu({ user }) {
return (
<div className="flex items-center space-x-2">
<img
src={user.avatar}
alt={user.name}
className="w-8 h-8 rounded-full"
/>
<span className="text-sm font-medium">{user.name}</span>
</div>
);
}
function LoginButton() {
return (
<button className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors">
Login
</button>
);
}
// Helper function to calculate cart totals
function calculateCartTotals(cartState) {
const products = productsStore.products;
let total = 0;
let itemCount = 0;
cartState.items.forEach(item => {
const product = products.find(p => p.id === item.productId);
if (product) {
total += product.price * item.quantity;
itemCount += item.quantity;
}
});
cartState.total = total;
cartState.itemCount = itemCount;
}
export default ShoppingApp;
Key Architecture Patterns
1. Domain-Separated Stores
// ✅ Good: Each domain has its own store
const productsStore = proxy({ /* products logic */ });
const cartStore = proxy({ /* cart logic */ });
const userStore = proxy({ /* user logic */ });
// Each store is initialized independently
useInitSync(productsStore, loadProducts);
useInitSync(cartStore, loadCart);
useInitSync(userStore, loadUser);
2. Independent Loading States
// Each store manages its own loading state
const { loading: productsLoading } = useStore(productsStore);
const { loading: userLoading } = useStore(userStore);
const { loading: cartLoading } = useStore(cartStore);
// UI can show different loading states
if (productsLoading) return <ProductsSkeleton />;
if (userLoading) return <UserSkeleton />;
3. Cross-Store Dependencies
// Cart initialization depends on user data
useInitSync(cartStore, async (state) => {
if (!user?.id) {
state.items = [];
return;
}
const cartItems = await fetchCart(user.id);
state.items = cartItems;
}, {
deps: [user?.id] // Re-run when user changes
});
4. Selective Subscriptions
// Only subscribe to what you need
const { itemCount } = useStore(cartStore, s => ({ itemCount: s.itemCount }));
const { user, isAuthenticated } = useStore(userStore, s => ({
user: s.user,
isAuthenticated: s.isAuthenticated
}));
Benefits of This Pattern
- Independence: Each store can load and update independently
- Performance: Components only re-render when their specific data changes
- Scalability: Easy to add new domains without affecting existing ones
- Testability: Each domain can be tested in isolation
- Maintainability: Clear separation of concerns
Alternative: Single Store Approach
For comparison, here's how the same functionality could be implemented with a single coordinated store:
const appStore = proxy({
products: { data: [], loading: true, error: null },
cart: { items: [], total: 0, loading: false },
user: { data: null, loading: true, isAuthenticated: false }
});
useInitSync(appStore, async (state) => {
// Load all data in parallel
const [products, user, cart] = await Promise.allSettled([
fetchProducts(),
fetchUserSession(),
fetchCart()
]);
// Handle results...
});
When to Use Multiple Stores
- Different loading patterns (some data loads immediately, some on demand)
- Independent update frequencies (user data rarely changes, cart changes often)
- Separate error handling (product errors shouldn't affect user experience)
- Different access patterns (some data is global, some is component-specific)
See Also
- useInitSync() API - Complete API reference
- Dashboard Example - Coordinated initialization pattern
- User Profile Example - Single store async pattern