Skip to content

State Management Pattern

Client-side state with Zustand.

When to Use

State Type Solution
Server data TanStack Query
UI state (modals, menus) React useState
Shared client state Zustand
Form state React Hook Form

Store Structure

type AuthState = {
  // State
  isLoggedIn: boolean;
  userId: string | null;
  role: string | null;
  groupId: string | null;

  // Actions
  setAccessToken: (token: string) => void;
  clearAuthState: () => void;
  logIn: (username: string, password: string) => Promise<LoginResult>;
  logOut: () => Promise<void>;

  // Computed
  isAdmin: () => boolean;
};

Creating a Store

export const useAuth = create<AuthState>((set, get) => ({
  isLoggedIn: false,
  userId: null,
  role: null,
  groupId: null,

  setAccessToken: (token: string) => {
    storage.set(AUTH.ACCESS_TOKEN_KEY, token);
    const data = parseToken(token);
    set({
      isLoggedIn: true,
      userId: data.userId,
      role: data.role,
      groupId: data.groupId,
    });
  },

  clearAuthState: () => {
    storage.remove(AUTH.ACCESS_TOKEN_KEY);
    set({ isLoggedIn: false, userId: null, role: null, groupId: null });
  },

  isAdmin: () => get().role === ROLES.ADMIN,
}));

Using Selectors

// Select specific state (prevents unnecessary re-renders)
const isLoggedIn = useAuth((state) => state.isLoggedIn);
const isAdmin = useAuth((state) => state.isAdmin());

// Select multiple values
const { userId, groupId } = useAuth((state) => ({
  userId: state.userId,
  groupId: state.groupId,
}));

Persisted State

// Use storage wrapper, not raw localStorage
import { storage } from "../utils/storage";

storage.set(key, value);
storage.get(key);
storage.remove(key);