Overview
Villa Buena uses Zustand for client-side state management, chosen for its simplicity, minimal boilerplate, and excellent TypeScript support. All stores use the persist middleware to sync with localStorage.
Zustand provides a lightweight alternative to Redux with a hooks-based API that feels natural in React applications.
Store Architecture
The application has three primary stores, each managing a distinct domain:
Cart Store Shopping cart items and quantities
User Store Checkout data and order history
UI Store UI preferences and transient state
Cart Store
Located in src/store/useCartStore.js, this store manages shopping cart operations.
Store Definition
import { create } from "zustand" ;
import { persist } from "zustand/middleware" ;
export const useCartStore = create (
persist (
( set ) => ({
cart: [],
addToCart : ( product ) =>
set (( state ) => {
const existing = state . cart . find (
( item ) => item . id === product . id
);
if ( existing ) {
return {
cart: state . cart . map (( item ) =>
item . id === product . id
? { ... item , qty: item . qty + 1 }
: item
),
};
}
return {
cart: [ ... state . cart , { ... product , qty: 1 }],
};
}),
increaseQty : ( id ) =>
set (( state ) => ({
cart: state . cart . map (( item ) =>
item . id === id
? { ... item , qty: item . qty + 1 }
: item
),
})),
decreaseQty : ( id ) =>
set (( state ) => ({
cart: state . cart
. map (( item ) =>
item . id === id
? { ... item , qty: item . qty - 1 }
: item
)
. filter (( item ) => item . qty > 0 ),
})),
removeFromCart : ( id ) =>
set (( state ) => ({
cart: state . cart . filter (
( item ) => item . id !== id
),
})),
clearCart : () =>
set (() => ({
cart: [],
})),
}),
{
name: "cart-storage" ,
}
)
);
Cart State Shape
interface CartItem {
id : number ;
qty : number ;
// ...other product properties
}
interface CartStore {
cart : CartItem [];
addToCart : ( product : Product ) => void ;
increaseQty : ( id : number ) => void ;
decreaseQty : ( id : number ) => void ;
removeFromCart : ( id : number ) => void ;
clearCart : () => void ;
}
Key Features
Duplicate Detection
When adding a product that already exists in the cart (useCartStore.js:11-12), the quantity is incremented instead of creating a duplicate entry.
Automatic Removal
The decreaseQty action automatically removes items when quantity reaches zero (useCartStore.js:47).
Persistence
Cart data persists to localStorage under the key cart-storage (useCartStore.js:63), surviving page refreshes.
Usage Example
import { useCartStore } from '../store/useCartStore' ;
function ProductCard ({ product }) {
const addToCart = useCartStore ( state => state . addToCart );
return (
< button onClick = { () => addToCart ( product ) } >
Add to Cart
</ button >
);
}
Zustand’s selector pattern useCartStore(state => state.addToCart) ensures components only re-render when the selected slice of state changes.
User Store
The user store (src/store/useUserStore.js) manages checkout information and order history.
Store Structure
export const useUserStore = create (
persist (
( set ) => ({
shipping: {
fullName: "" ,
address: "" ,
city: "" ,
},
payment: {
cardNumber: "" ,
expiryDate: "" ,
cvc: "" ,
},
setShipping : ( data ) =>
set (( state ) => ({
shipping: { ... state . shipping , ... data },
})),
setPayment : ( data ) =>
set (( state ) => ({
payment: { ... state . payment , ... data },
})),
hydrateFromAuth0 : ( user ) =>
set (( state ) => ({
shipping: {
... state . shipping ,
fullName: state . shipping . fullName || user ?. name || "" ,
},
})),
orders: [],
addOrder : ( order ) =>
set (( state ) => ({
orders: [ ... state . orders , order ],
})),
}),
{
name: "user-checkout-storage" ,
},
),
);
Auth0 Integration
The hydrateFromAuth0 action (useUserStore.js:29-35) integrates with Auth0 user data, pre-filling the shipping form with the authenticated user’s name.
hydrateFromAuth0 : ( user ) =>
set (( state ) => ({
shipping: {
... state . shipping ,
fullName: state . shipping . fullName || user ?. name || "" ,
},
}))
Multi-Step Checkout
The store enables a multi-step checkout flow:
Shipping Step : Update with setShipping(data)
Payment Step : Update with setPayment(data)
Order Completion : Save order with addOrder(order)
UI Store
The UI store (src/store/uiStore.js) handles ephemeral UI state and user preferences.
Complete Implementation
export const useUIStore = create (
persist (
( set ) => ({
/*darkmode*/
darkMode: false ,
toggleDarkMode : () =>
set (( state ) => ({
darkMode: ! state . darkMode ,
})),
/*cart drawer*/
isCartOpen: false ,
openCart : () => set ({ isCartOpen: true }),
closeCart : () => set ({ isCartOpen: false }),
/*toast*/
toast: null ,
toastKey: 0 ,
showToast : ( message ) => {
set (( state ) => ({
toast: message ,
toastKey: state . toastKey + 1 ,
}));
},
hideToast : () => set ({ toast: null }),
}),
{
name: "ui-storage" ,
partialize : ( state ) => ({
darkMode: state . darkMode ,
}),
},
),
);
Selective Persistence
The partialize option (uiStore.js:34-36) selectively persists only the darkMode preference:
partialize : ( state ) => ({
darkMode: state . darkMode ,
})
Cart drawer state and toast notifications are intentionally ephemeral and don’t persist across sessions.
Toast State Pattern
The toast implementation uses a toastKey counter (uiStore.js:21-26) to trigger new animations even when showing the same message consecutively:
showToast : ( message ) => {
set (( state ) => ({
toast: message ,
toastKey: state . toastKey + 1 ,
}));
}
Dark Mode Integration
The Layout component (src/app/Layout.jsx:9-14) syncs dark mode state with CSS classes:
const darkMode = useUIStore (( state ) => state . darkMode );
useEffect (() => {
document . body . classList . remove ( "light-mode" , "dark-mode" );
document . body . classList . add ( darkMode ? "dark-mode" : "light-mode" );
}, [ darkMode ]);
Best Practices
Use Selectors
Always select specific slices of state to minimize re-renders: const cart = useCartStore ( state => state . cart );
Immutable Updates
Zustand requires immutable state updates. Always return new objects/arrays: set (( state ) => ({ cart: [ ... state . cart , newItem ] }))
Separate Concerns
Keep domain-specific state in separate stores rather than one monolithic store.
Persist Strategically
Use partialize to persist only necessary data and avoid localStorage bloat.
State Flow Diagram
Zustand’s middleware system allows easy integration with DevTools, logging, and other state management tools.