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 - MUST use useShallow to prevent infinite re-renders
import { useShallow } from "zustand/shallow";

const { userId, groupId } = useAuth(
  useShallow((state) => ({
    userId: state.userId,
    groupId: state.groupId,
  }))
);

Object selectors cause infinite loops

Without useShallow, returning an object from a selector creates a new reference every render, causing infinite re-renders. Always wrap object-returning selectors with useShallow.

Persisted State

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

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