Global State Management (Zustand)
Relevant source files
The following files were used as context for generating this wiki page:
This project utilizes Zustand for managing global client-side state. The architecture principles designate Zustand for handling state related to the cart, user authentication, and UI. This approach centralizes cross-component state logic, providing a single source of truth and simplifying state management compared to prop drilling or context alone. The state management strategy also includes local component state via React hooks and a plan to use React Query for server state.
The core of this system resides in the stores/ directory, which contains individual stores for different features. Each store is a self-contained module with its own state, actions, and persistence logic. This documentation covers the implementation and integration of the primary Zustand stores: auth.store.ts and cart.store.ts.
Sources: README.md:29-32, README.md:50-54, package.json:42
Architecture Overview
The global state management architecture is designed to decouple state logic from UI components. Components interact with Zustand stores through hooks. The stores, in turn, can be simple state containers or can orchestrate more complex flows, such as making API calls via the api-client. Some stores also persist their state to localStorage to maintain state across browser sessions.
The following diagram illustrates the high-level data flow between UI components, Zustand stores, and the backend API.
Authentication Store (auth.store.ts)
The auth.store.ts is responsible for managing authentication tokens and derived authentication status. It is designed to only handle tokens, with user profile data being managed by React Query.
Sources: stores/auth.store.ts:1-5
State and Interface
The store maintains the accessToken, refreshToken, and the userId extracted from the JWT.
Sources: stores/auth.store.ts:36-45
| State Property | Type | Description |
|---|---|---|
accessToken | string | null | The JWT access token for API authentication. |
refreshToken | string | null | The token used to obtain a new access token. |
userId | string | null | The user's unique identifier, decoded from the JWT sub claim. |
Sources: stores/auth.store.ts:37-39
Key Actions
| Action | Description |
|---|---|
setTokens(accessToken, refreshToken) | Sets both tokens in the store. It decodes the accessToken to extract and store the userId. |
clearTokens() | Clears all authentication data from the store, effectively logging the user out. |
isAuthenticated() | A utility function that returns true if an accessToken is present. |
getUserId() | Returns the cached userId or attempts to decode it from the current accessToken. |
getUserRole() | Decodes the accessToken to return the user's role (ADMIN or CUSTOMER). |
Sources: stores/auth.store.ts:51-93
Persistence
The auth store uses the persist middleware from Zustand to save authentication tokens to localStorage. This ensures that the user remains logged in across page reloads.
- Storage Key:
auth-store - Persisted State: The
partializefunction ensures that onlyaccessToken,refreshToken, anduserIdare saved tolocalStorage.
Sources: stores/auth.store.ts:95-103
Integration with API Client
The api-client.ts is tightly integrated with the auth.store.
- Request Interceptor: Before any API request is sent, an interceptor retrieves the
accessTokendirectly from the auth store usinguseAuthStore.getState().accessTokenand adds it to theAuthorizationheader. - Response Interceptor (Token Refresh): If an API request returns a 401 Unauthorized error, a response interceptor attempts to refresh the token. It retrieves the
refreshTokenfrom the store, calls the/auth/refreshendpoint, and if successful, updates the tokens in the store usingsetTokensbefore retrying the original request. If the refresh fails, it callsclearTokensand redirects the user to the login page.
The following diagram shows the token refresh sequence:
Cart Store (cart.store.ts)
The cart.store.ts manages the state of the user's shopping cart, including its items, loading states, and interactions with the backend cart API.
Sources: stores/cart.store.ts
State and Interface
The store holds the cart object, a loading flag for asynchronous operations, and an error message.
| State Property | Type | Description |
|---|---|---|
cart | Cart | null | The main cart object, containing items and total price. null if the cart is empty or not yet fetched. |
loading | boolean | Indicates if a cart operation (e.g., addItem, fetchCart) is in progress. |
error | string | null | Stores an error message if a cart operation fails. |
Sources: stores/cart.store.ts:10-12
Key Actions
All major actions are asynchronous, interact with the cartApi, and provide user feedback via sonner toast notifications.
| Action | Description |
|---|---|
fetchCart() | Fetches the user's cart from the backend and updates the store. |
addItem(productId, quantity) | Adds a specified quantity of a product to the cart. Prevents multiple simultaneous requests. |
updateItem(itemId, quantity) | Updates the quantity of an existing item in the cart. |
removeItem(itemId) | Removes an item from the cart. |
clearCart() | Removes all items from the cart. |
getItemCount() | A synchronous utility that returns the total number of items (sum of quantities) in the cart. |
getTotal() | A synchronous utility that returns the total price (totalInt) of the cart. |
reset() | Resets the cart store to its initial state. |
Sources: stores/cart.store.ts:15-115
Add to Cart Flow
The following sequence diagram illustrates the process of adding an item to the cart, from the user action to the final state update.
Sources: stores/cart.store.ts:47-70
Persistence
Similar to the auth store, the cart store uses the persist middleware to save the cart state.
- Storage Key:
cart-store - Persisted State: The
partializefunction is configured to only persist thecartobject itself, excludingloadinganderrorstates.
Sources: stores/cart.store.ts:117-120
Consuming Stores in Components
Zustand stores are consumed in React components by calling the store hook. For example, the AdminLayout component uses useAuthStore to get the current user's role and display their initials.
// app/admin/layout.tsx
export default function AdminLayout({ children }: AdminLayoutProps): React.ReactElement {
// ...
const getUserRole = useAuthStore((state) => state.getUserRole);
// ...
const userRole = getUserRole();
const userInitials = userRole === "ADMIN" ? "A" : "U";
return (
// ...
<AvatarFallback>{userInitials}</AvatarFallback>
// ...
);
}
This example demonstrates how a component can subscribe to a part of the store's state or its actions. The selector function (state) => state.getUserRole ensures that the component only re-renders when the selected piece of state changes, which is an optimized approach.
Sources: app/admin/layout.tsx:29-35, app/admin/layout.tsx:50
Summary
The project's global state management is effectively handled by Zustand, with a clear separation of concerns between different stores. The auth.store is critical for security and API communication, providing a robust token management and refresh mechanism. The cart.store manages user shopping data, handling asynchronous operations and persistence gracefully. The integration of these stores with the API client and UI components demonstrates a scalable and maintainable architecture for managing global state in the application.
Sources: README.md:50-54, stores/auth.store.ts, stores/cart.store.ts