Skip to main content

State Management

Client-side state management using Zustand and server state with TanStack Query.

Architecture Overview

┌─────────────────────────────────────────────────────────────────────────┐
│ State Management │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ CLIENT STATE (Zustand) SERVER STATE (React Query) │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ auth-store │ │ Conversations │ │
│ │ • user │ │ • queries │ │
│ │ • isAuthenticated │ │ • mutations │ │
│ └────────────────────┘ │ • cache │ │
│ ┌────────────────────┐ └────────────────────┘ │
│ │ workspace-store │ ┌────────────────────┐ │
│ │ • workspaces │ │ Analytics │ │
│ │ • activeWorkspace │ │ • stats │ │
│ └────────────────────┘ │ • usage data │ │
│ ┌────────────────────┐ └────────────────────┘ │
│ │ ui-store │ │
│ │ • sidebar │ │
│ │ • modals │ │
│ └────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

Zustand Stores

Auth Store

Manages user authentication and session state.

// lib/stores/auth-store.ts

interface User {
_id: string;
email: string;
name: string;
role: 'user' | 'admin';
status: 'active' | 'inactive';
}

interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
isInitialized: boolean;
}

interface AuthActions {
setUser: (user: User | null) => void;
updateUser: (userData: Partial<User>) => void;
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string, name: string) => Promise<void>;
logout: (all?: boolean) => Promise<void>;
fetchUser: () => Promise<void>;
initialize: () => Promise<void>;
}

export const useAuthStore = create<AuthState & AuthActions>()(
persist(
(set, get) => ({
user: null,
isAuthenticated: false,
isLoading: false,
isInitialized: false,

setUser: (user) => set({
user,
isAuthenticated: !!user,
}),

login: async (email, password) => {
set({ isLoading: true });
try {
const { data } = await authApi.login(email, password);
set({ user: data.user, isAuthenticated: true });
} finally {
set({ isLoading: false });
}
},

logout: async (all = false) => {
await authApi.logout(all);
set({ user: null, isAuthenticated: false });
// Clear other stores
useWorkspaceStore.getState().clearWorkspaces();
},

initialize: async () => {
if (get().isInitialized) return;
try {
await get().fetchUser();
} finally {
set({ isInitialized: true });
}
},
}),
{
name: 'auth-storage',
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
}
)
);

Helper Hooks

// Check if user is admin
export function useIsAdmin() {
return useAuthStore((state) => state.user?.role === 'admin');
}

Workspace Store

Manages workspace selection and membership.

// lib/stores/workspace-store.ts

interface WorkspaceWithMembership {
_id: string;
name: string;
description?: string;
owner: string;
role: 'owner' | 'admin' | 'member' | 'viewer';
permissions: {
canQuery: boolean;
canViewSources: boolean;
canInvite: boolean;
canManageSync: boolean;
canEditSettings: boolean;
};
}

interface WorkspaceState {
workspaces: WorkspaceWithMembership[];
activeWorkspaceId: string | null;
isLoading: boolean;
error: string | null;
}

interface WorkspaceActions {
setWorkspaces: (workspaces: WorkspaceWithMembership[]) => void;
setActiveWorkspace: (id: string | null) => void;
fetchWorkspaces: () => Promise<void>;
createWorkspace: (name: string, description?: string) => Promise<void>;
updateWorkspace: (id: string, data: Partial<WorkspaceWithMembership>) => Promise<void>;
deleteWorkspace: (id: string) => Promise<void>;
clearWorkspaces: () => void;
}

export const useWorkspaceStore = create<WorkspaceState & WorkspaceActions>()(
persist(
(set, get) => ({
workspaces: [],
activeWorkspaceId: null,
isLoading: false,
error: null,

setActiveWorkspace: (id) => {
set({ activeWorkspaceId: id });
// Persist to localStorage for API header
if (id) {
localStorage.setItem('activeWorkspaceId', id);
} else {
localStorage.removeItem('activeWorkspaceId');
}
},

fetchWorkspaces: async () => {
set({ isLoading: true, error: null });
try {
const { data } = await workspacesApi.getWorkspaces();
set({ workspaces: data.workspaces });

// Auto-select first workspace if none active
const { activeWorkspaceId } = get();
if (!activeWorkspaceId && data.workspaces.length > 0) {
get().setActiveWorkspace(data.workspaces[0]._id);
}
} catch (error) {
set({ error: 'Failed to load workspaces' });
} finally {
set({ isLoading: false });
}
},

clearWorkspaces: () => {
set({
workspaces: [],
activeWorkspaceId: null,
});
localStorage.removeItem('activeWorkspaceId');
},
}),
{
name: 'workspace-storage',
partialize: (state) => ({
activeWorkspaceId: state.activeWorkspaceId,
}),
}
)
);

Selector Hooks

// Get active workspace details
export function useActiveWorkspace() {
return useWorkspaceStore((state) =>
state.workspaces.find((w) => w._id === state.activeWorkspaceId)
);
}

// Get current workspace role
export function useWorkspaceRole() {
const workspace = useActiveWorkspace();
return {
role: workspace?.role,
isOwner: workspace?.role === 'owner',
isAdmin: workspace?.role === 'admin',
isMember: workspace?.role === 'member',
isViewer: workspace?.role === 'viewer',
};
}

// Get current workspace permissions
export function useWorkspacePermissions() {
const workspace = useActiveWorkspace();
return workspace?.permissions ?? {
canQuery: false,
canViewSources: false,
canInvite: false,
canManageSync: false,
canEditSettings: false,
};
}

UI Store

Manages UI state like sidebar and modals.

// lib/stores/ui-store.ts

export const MODAL_IDS = {
CREATE_WORKSPACE: 'create-workspace',
INVITE_MEMBER: 'invite-member',
DELETE_CONFIRMATION: 'delete-confirmation',
SETTINGS: 'settings',
} as const;

type ModalId = typeof MODAL_IDS[keyof typeof MODAL_IDS];

interface UIState {
sidebarOpen: boolean;
sidebarCollapsed: boolean;
activeModal: ModalId | null;
modalData: Record<string, unknown> | null;
isMobile: boolean;
}

interface UIActions {
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
setSidebarCollapsed: (collapsed: boolean) => void;
openModal: (modalId: ModalId, data?: Record<string, unknown>) => void;
closeModal: () => void;
setIsMobile: (isMobile: boolean) => void;
}

export const useUIStore = create<UIState & UIActions>((set) => ({
sidebarOpen: true,
sidebarCollapsed: false,
activeModal: null,
modalData: null,
isMobile: false,

toggleSidebar: () =>
set((state) => ({ sidebarOpen: !state.sidebarOpen })),

setSidebarOpen: (open) => set({ sidebarOpen: open }),

setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),

openModal: (modalId, data = null) =>
set({ activeModal: modalId, modalData: data }),

closeModal: () => set({ activeModal: null, modalData: null }),

setIsMobile: (isMobile) => set({ isMobile }),
}));

React Query Setup

Query Client Configuration

// components/providers/query-provider.tsx

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes (garbage collection)
retry: 1,
refetchOnWindowFocus: false,
refetchOnReconnect: true,
},
mutations: {
retry: 0,
},
},
});

export function QueryProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}

Query Examples

Fetching Conversations

// Using React Query for server state

function useConversations() {
const workspaceId = useWorkspaceStore((s) => s.activeWorkspaceId);

return useQuery({
queryKey: ['conversations', workspaceId],
queryFn: () => conversationsApi.getConversations(),
enabled: !!workspaceId,
});
}

function useConversation(id: string) {
return useQuery({
queryKey: ['conversation', id],
queryFn: () => conversationsApi.getConversation(id),
enabled: !!id,
});
}

Mutations

function useCreateConversation() {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (title: string) =>
conversationsApi.createConversation(title),
onSuccess: () => {
// Invalidate conversations list
queryClient.invalidateQueries({ queryKey: ['conversations'] });
},
});
}

function useDeleteConversation() {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (id: string) => conversationsApi.deleteConversation(id),
onSuccess: (_, id) => {
// Remove from cache immediately
queryClient.setQueryData(['conversations'], (old: Conversation[]) =>
old?.filter((c) => c._id !== id)
);
},
});
}

Cache Invalidation

Real-time events trigger cache invalidation:

// components/providers/socket-provider.tsx

useEffect(() => {
socket.on('conversation:updated', () => {
queryClient.invalidateQueries({ queryKey: ['conversations'] });
});

socket.on('sync:completed', () => {
queryClient.invalidateQueries({ queryKey: ['documents'] });
queryClient.invalidateQueries({ queryKey: ['analytics'] });
});

return () => {
socket.off('conversation:updated');
socket.off('sync:completed');
};
}, [socket, queryClient]);

State Synchronization

Cross-Store Communication

// Logout clears all stores
const logout = async () => {
await authApi.logout();
useAuthStore.getState().setUser(null);
useWorkspaceStore.getState().clearWorkspaces();
queryClient.clear(); // Clear all cached queries
};

Persist Middleware

Zustand's persist middleware saves state to localStorage:

persist(
(set, get) => ({ ... }),
{
name: 'store-name',
partialize: (state) => ({
// Only persist specific fields
user: state.user,
}),
}
)

Hydration

Handle hydration mismatch between server and client:

function useHydration() {
const [hydrated, setHydrated] = useState(false);

useEffect(() => {
setHydrated(true);
}, []);

return hydrated;
}

function Component() {
const hydrated = useHydration();
const { user } = useAuthStore();

if (!hydrated) {
return <Skeleton />;
}

return <UserDisplay user={user} />;
}

Best Practices

1. Selective Subscriptions

// Good - only re-renders when user changes
const user = useAuthStore((state) => state.user);

// Bad - re-renders on any store change
const { user, isLoading, error } = useAuthStore();

2. Derived State

// Create selectors for derived state
export const selectIsOwner = (state: WorkspaceState) =>
state.workspaces.find((w) => w._id === state.activeWorkspaceId)?.role === 'owner';

// Use in component
const isOwner = useWorkspaceStore(selectIsOwner);

3. Action Composition

// Compose complex actions
const initializeApp = async () => {
await useAuthStore.getState().initialize();
if (useAuthStore.getState().isAuthenticated) {
await useWorkspaceStore.getState().fetchWorkspaces();
}
};

4. Query Key Conventions

// Hierarchical query keys
['conversations'] // List
['conversations', id] // Single item
['conversations', id, 'messages'] // Nested resource
['analytics', workspaceId, period] // With parameters