Files
gh-jezweb-claude-skills-ski…/templates/computed-store.ts
2025-11-30 08:25:53 +08:00

215 lines
5.6 KiB
TypeScript

/**
* Zustand Store with Computed/Derived Values
*
* Use when:
* - Need values calculated from state
* - Want to avoid storing redundant data
* - Need efficient memoization
*
* Pattern: Compute in selectors, not in store
*
* Benefits:
* - Cleaner state (only source data)
* - Automatic recomputation when dependencies change
* - Components only re-render when result changes
*
* Learn more: See SKILL.md Common Patterns section
*/
import { create } from 'zustand'
interface Product {
id: string
name: string
price: number
category: string
inStock: boolean
}
interface CartItem {
productId: string
quantity: number
}
interface ComputedStore {
// Source state (stored)
products: Product[]
cart: CartItem[]
taxRate: number
discountPercent: number
// Actions
addProduct: (product: Product) => void
addToCart: (productId: string, quantity: number) => void
removeFromCart: (productId: string) => void
setTaxRate: (rate: number) => void
setDiscount: (percent: number) => void
}
export const useComputedStore = create<ComputedStore>()((set) => ({
// Initial state
products: [],
cart: [],
taxRate: 0.1, // 10%
discountPercent: 0,
// Actions
addProduct: (product) =>
set((state) => ({
products: [...state.products, product],
})),
addToCart: (productId, quantity) =>
set((state) => {
const existing = state.cart.find((item) => item.productId === productId)
if (existing) {
return {
cart: state.cart.map((item) =>
item.productId === productId
? { ...item, quantity: item.quantity + quantity }
: item
),
}
}
return {
cart: [...state.cart, { productId, quantity }],
}
}),
removeFromCart: (productId) =>
set((state) => ({
cart: state.cart.filter((item) => item.productId !== productId),
})),
setTaxRate: (rate) => set({ taxRate: rate }),
setDiscount: (percent) => set({ discountPercent: percent }),
}))
// ============================================================================
// COMPUTED/DERIVED SELECTORS (put these in separate file for reuse)
// ============================================================================
/**
* Selector: Get cart items with product details
*/
export const selectCartWithDetails = (state: ComputedStore) =>
state.cart.map((item) => {
const product = state.products.find((p) => p.id === item.productId)
return {
...item,
product,
subtotal: product ? product.price * item.quantity : 0,
}
})
/**
* Selector: Calculate subtotal (before tax and discount)
*/
export const selectSubtotal = (state: ComputedStore) =>
state.cart.reduce((sum, item) => {
const product = state.products.find((p) => p.id === item.productId)
return sum + (product ? product.price * item.quantity : 0)
}, 0)
/**
* Selector: Calculate discount amount
*/
export const selectDiscountAmount = (state: ComputedStore) => {
const subtotal = selectSubtotal(state)
return subtotal * (state.discountPercent / 100)
}
/**
* Selector: Calculate tax amount
*/
export const selectTaxAmount = (state: ComputedStore) => {
const subtotal = selectSubtotal(state)
const discountAmount = selectDiscountAmount(state)
const afterDiscount = subtotal - discountAmount
return afterDiscount * state.taxRate
}
/**
* Selector: Calculate final total
*/
export const selectTotal = (state: ComputedStore) => {
const subtotal = selectSubtotal(state)
const discountAmount = selectDiscountAmount(state)
const taxAmount = selectTaxAmount(state)
return subtotal - discountAmount + taxAmount
}
/**
* Selector: Get products by category
*/
export const selectProductsByCategory = (category: string) => (state: ComputedStore) =>
state.products.filter((product) => product.category === category)
/**
* Selector: Get in-stock products only
*/
export const selectInStockProducts = (state: ComputedStore) =>
state.products.filter((product) => product.inStock)
/**
* Selector: Count items in cart
*/
export const selectCartItemCount = (state: ComputedStore) =>
state.cart.reduce((sum, item) => sum + item.quantity, 0)
/**
* Usage in components:
*
* function CartSummary() {
* // Each selector only causes re-render when its result changes
* const subtotal = useComputedStore(selectSubtotal)
* const discount = useComputedStore(selectDiscountAmount)
* const tax = useComputedStore(selectTaxAmount)
* const total = useComputedStore(selectTotal)
* const itemCount = useComputedStore(selectCartItemCount)
*
* return (
* <div>
* <h3>Cart ({itemCount} items)</h3>
* <p>Subtotal: ${subtotal.toFixed(2)}</p>
* <p>Discount: -${discount.toFixed(2)}</p>
* <p>Tax: ${tax.toFixed(2)}</p>
* <h4>Total: ${total.toFixed(2)}</h4>
* </div>
* )
* }
*
* function ProductList() {
* const inStockProducts = useComputedStore(selectInStockProducts)
*
* return (
* <ul>
* {inStockProducts.map((product) => (
* <li key={product.id}>{product.name} - ${product.price}</li>
* ))}
* </ul>
* )
* }
*
* function CategoryFilter() {
* const electronicsProducts = useComputedStore(selectProductsByCategory('electronics'))
*
* return <div>{electronicsProducts.length} electronics</div>
* }
*
* PERFORMANCE TIP:
* For expensive computations, use useMemo in the component:
*
* function ExpensiveComputation() {
* const data = useComputedStore((state) => state.products)
*
* const result = useMemo(() => {
* // Expensive calculation
* return data.map(...).filter(...).reduce(...)
* }, [data])
*
* return <div>{result}</div>
* }
*/