Skip to main content

Introduction

The Auction Platform uses Zustand for state management. Zustand provides a lightweight, hook-based API for managing global state without the complexity of traditional state management libraries.

Why Zustand?

Simple API

Minimal boilerplate with a straightforward, hook-based interface

No Providers

No context providers needed - stores work out of the box

Type-Safe

Full TypeScript support with excellent type inference

Performant

Fine-grained subscriptions prevent unnecessary re-renders

Store Organization

Stores are located in src/app/store/ and organized by domain:
app/store/
└── auth/
    └── auth.store.ts
Each store follows a consistent structure with state and actions.

Auth Store Example

The auth store manages authentication state and user session:
import { create } from "zustand";
import type { AuthState, User } from "../../../features/auth/types/auth.types";
import { apiGET } from "../../api/client";

type AuthStore = AuthState & {
  init: () => Promise<void>;
  login: (user: User) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthStore>((set, get) => ({
  // Initial State
  isAuthenticated: false,
  isOnboarded: false,
  user: null,
  initialized: false,

  // Actions
  init: async () => {
    if (get().initialized) return;

    try {
      const user: User = await apiGET("/auth/me");
      set({
        user,
        isAuthenticated: true,
        isOnboarded: user.is_onboarded,
        initialized: true,
      });
    } catch {
      set({
        user: null,
        isAuthenticated: false,
        isOnboarded: false,
        initialized: true,
      });
    }
  },

  login: (user) =>
    set({
      user,
      isAuthenticated: true,
      isOnboarded: user.is_onboarded,
      initialized: true,
    }),

  logout: () =>
    set({
      user: null,
      isAuthenticated: false,
      isOnboarded: false,
      initialized: true,
    }),
}));

Store Structure

Each Zustand store follows this pattern:
1

Define Types

Create TypeScript types for state and the complete store interface.
2

Initial State

Define the initial state values in the store creator function.
3

Actions

Add action methods that modify state using set() and read state using get().
4

Export Hook

Export the store as a custom hook (e.g., useAuthStore).

Using Stores in Components

Subscribe to Entire Store

import { useAuthStore } from '@app/store/auth/auth.store';

function UserProfile() {
  const authStore = useAuthStore();
  
  if (!authStore.isAuthenticated) {
    return <div>Please log in</div>;
  }
  
  return <div>Welcome, {authStore.user?.name}</div>;
}
Subscribing to the entire store causes re-renders on any state change. Use selectors for better performance.
import { useAuthStore } from '@app/store/auth/auth.store';

function UserProfile() {
  // Only re-renders when user changes
  const user = useAuthStore(state => state.user);
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
  
  if (!isAuthenticated) {
    return <div>Please log in</div>;
  }
  
  return <div>Welcome, {user?.name}</div>;
}
Using selectors ensures components only re-render when the specific state they depend on changes.

Access Store Outside React

Stores can be accessed outside React components:
import { useAuthStore } from '@app/store/auth/auth.store';

// Get current state
const state = useAuthStore.getState();

// Call actions
useAuthStore.getState().logout();

// Subscribe to changes
const unsubscribe = useAuthStore.subscribe(
  state => console.log('State changed:', state)
);
This pattern is used in:
  • API client for authentication checks (src/app/api/client.ts:28)
  • Route guards in TanStack Router
  • Application initialization (src/main.tsx:17)

State Initialization

The auth store is initialized before the application renders:
async function bootstrap() {
  // Initialize auth state from server
  await useAuthStore.getState().init()

  const router = createRouter({ routeTree })

  createRoot(document.getElementById('root')!).render(
    <StrictMode>
      <ErrorBoundary>
        <ThemeProvider>
          <RouterProvider router={router} />
        </ThemeProvider>
      </ErrorBoundary>
    </StrictMode>,
  )
}

bootstrap();
The init() action fetches the current user session from the server, ensuring auth state is ready before routing begins.

Common Patterns

Async Actions

Stores can contain async actions that fetch data and update state:
export const useDataStore = create<DataStore>((set, get) => ({
  data: null,
  loading: false,
  error: null,

  fetchData: async () => {
    set({ loading: true, error: null });
    
    try {
      const data = await apiGET('/data');
      set({ data, loading: false });
    } catch (error) {
      set({ 
        error: error.message, 
        loading: false 
      });
    }
  },
}));

Computed Values

Derive computed values from state:
export const useCartStore = create<CartStore>((set, get) => ({
  items: [],
  
  addItem: (item) => set(state => ({
    items: [...state.items, item]
  })),
  
  // Computed getter
  getTotal: () => {
    return get().items.reduce((sum, item) => sum + item.price, 0);
  },
}));

Immer for Complex Updates

For complex nested state updates, use the immer middleware:
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

export const useComplexStore = create(
  immer<ComplexStore>((set) => ({
    nested: {
      deep: {
        value: 0
      }
    },
    
    updateDeep: (newValue) => set((state) => {
      // Mutate draft state directly with immer
      state.nested.deep.value = newValue;
    }),
  }))
);

Store Actions vs Services

Use for:
  • Simple state updates
  • State transformations
  • Synchronous operations
  • Cross-feature state coordination
logout: () => set({ user: null, isAuthenticated: false })

DevTools Integration

Zustand integrates with Redux DevTools for debugging:
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

export const useAuthStore = create(
  devtools<AuthStore>(
    (set, get) => ({
      // ... store definition
    }),
    { name: 'AuthStore' }
  )
);
Install the Redux DevTools Extension to inspect state changes, time-travel debug, and track actions.

Best Practices

Always use selectors to subscribe to specific state slices and prevent unnecessary re-renders.
Create separate stores for different domains (auth, cart, settings) rather than one large store.
Define actions in the same store file to keep related logic together.
Use TypeScript for full type safety on state and actions.
Handle async operations in store actions, but consider moving complex logic to services.

Performance Optimization

Shallow Equality

For selecting multiple values, use shallow equality:
import { shallow } from 'zustand/shallow';

function Component() {
  const { user, isAuthenticated } = useAuthStore(
    state => ({ 
      user: state.user, 
      isAuthenticated: state.isAuthenticated 
    }),
    shallow
  );
}

Transient Updates

For high-frequency updates that don’t need to trigger re-renders:
const updatePosition = useStore.getState().updatePosition;

// Updates state without triggering subscribers
useStore.setState({ x: 100, y: 200 }, true);

Architecture Overview

Learn about the overall architecture

API Client

Understand how stores interact with the API

Routing

See how stores are used in route guards

Zustand Docs

Official Zustand documentation

Build docs developers (and LLMs) love