State Management
A deep dive into Zustand usage for managing app state in the marketplace starter kit.
Introduction
Zustand is a small, fast, and scalable state management solution that plays a central role in how state is managed across your app. Unlike Redux or Context, Zustand avoids unnecessary boilerplate and rerenders, making it ideal for real-time UIs and complex flows like listing creation, messaging, and user session handling.
This document provides a technical breakdown of how Zustand is used to manage the global state of your app, based on your actual source code.
Store Breakdown
useCreateListingStore
This store handles the state of the multi-step listing creation flow. This is not just UI state — it tracks and persists form data that can later be submitted to the server.
Real usage includes:
- Tracking the current step (
step
) - Storing all form inputs (
data
) - Mutating data across steps
- Resetting state when the flow is cancelled or completed
const useCreateListingStore = create<{
step: number;
data: Partial<ListingFormData>;
setStep: (n: number) => void;
updateData: (input: Partial<ListingFormData>) => void;
reset: () => void;
}>((set) => ({
step: 0,
data: {},
setStep: (step) => set({ step }),
updateData: (input) =>
set((state) => ({ data: { ...state.data, ...input } })),
reset: () => set({ step: 0, data: {} }),
}));
Why this matters:
Instead of pushing form state into the backend every step, the app aggregates it in a central store, allowing a final submission with clean validation and API interaction. It also allows users to resume drafts seamlessly.
useSessionStore
This store helps manage a hybrid auth/session model, particularly useful when:
- A user can switch between personal and organizational (or page) identities
- You need to reflect session changes instantly across components without prop-drilling
Example behavior:
- Set the current
socialUserId
after login or account switch - Use this store throughout components to reflect the active user identity (e.g., when sending messages or creating listings)
Best Practices Observed
1. Avoiding Overfetching and Rerenders
Zustand selectors (useStore((s) => s.prop)
) are used to avoid unnecessary re-renders when only specific state changes. This is visible in the form stepper UI and input state mapping logic.
2. Reset Patterns
State reset is critical in multi-step flows:
useEffect(() => {
return () => useCreateListingStore.getState().reset();
}, []);
This ensures state doesn’t leak across sessions or page reloads — a common bug in form-heavy apps.
Middleware Usage
You have not used persist
or immer
, but adding them can improve UX:
- Use
persist
to keep draft data even after a full page refresh - Use
immer
to simplify nested updates (especially for objects like availability or pricing tiers)
import { create } from "zustand";
import { persist } from "zustand/middleware";
const useDraftStore = create(
persist(
(set) => ({
data: {},
setData: (input) =>
set((state) => ({ data: { ...state.data, ...input } })),
}),
{ name: "listing-draft" }
)
);
Recommendations Based on Real Code
-
Persist Draft Listings
Right now, the store resets on reload. Addzustand/middleware/persist
to persistuseCreateListingStore
between sessions. -
Decouple Step Logic
Consider separating visual UI steps (what users see) from logic validation steps (what must be validated before continuing). Right now they're tightly coupled. -
Type Safety
Type guards or enums for steps would prevent edge-case bugs when users go out of order. -
Devtools Middleware
Add Zustand Devtools during development to track state transitions live.
When to Use Zustand vs API
Use Zustand for:
- Short-lived UI state (multi-step forms, modal toggles)
- Globally accessed values (session, filters, user identity)
Don’t use it for:
- Canonical data (like listings or bookings) — always fetch those from the DB or cache
- Data that should survive logouts or sessions — store that on server or persistent layer