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);