# State Management Guide - Zustand + TanStack Query

## Document Overview

This guide explains how to effectively combine Zustand (client state) and TanStack Query v5 (server state) in ColaFlow's frontend architecture, with complete implementation examples.

**Key Principle**: Zustand manages authentication and UI state, TanStack Query manages all API data.

---

## Table of Contents

1. [Architecture Overview](#architecture-overview)
2. [Zustand Stores](#zustand-stores)
3. [TanStack Query Setup](#tanstack-query-setup)
4. [Custom Hooks](#custom-hooks)
5. [State Synchronization](#state-synchronization)
6. [Best Practices](#best-practices)
7. [Performance Optimization](#performance-optimization)

---

## Architecture Overview

### State Classification

| State Type | Technology | Persistence | Example |
|------------|------------|-------------|---------|
| **Authentication** | Zustand | Session Storage | User info, tenant info, access token |
| **UI State** | Zustand | Local Storage | Sidebar open/closed, theme, language |
| **Server Data** | TanStack Query | Memory (cache) | Projects, issues, MCP tokens |
| **Form State** | React Hook Form | Component local | Login form, create token form |

### Data Flow

```
User Action
    ↓
Component (UI Layer)
    ↓
    ├──→ Zustand Store (for auth/UI state)
    │    ├── setUser()
    │    ├── clearAuth()
    │    └── toggleSidebar()
    │
    └──→ TanStack Query Hook (for API data)
         ├── useLogin() → authService.login()
         ├── useMcpTokens() → mcpService.listTokens()
         └── useCreateMcpToken() → mcpService.createToken()
              ↓
         API Client (Axios)
              ↓
         Backend API (.NET 9)
```

---

## Zustand Stores

### 1. Auth Store

**File**: `stores/useAuthStore.ts`

```typescript
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

// Types
export interface User {
  id: string;
  email: string;
  fullName: string;
  role: 'Admin' | 'Member' | 'Viewer';
  authProvider: 'Local' | 'AzureAD' | 'Google' | 'Okta';
  externalUserId?: string;
  avatarUrl?: string;
}

export interface Tenant {
  id: string;
  slug: string;
  name: string;
  plan: 'Free' | 'Starter' | 'Professional' | 'Enterprise';
  status: 'Active' | 'Trial' | 'Suspended' | 'Cancelled';
  ssoEnabled: boolean;
  ssoProvider?: 'AzureAD' | 'Google' | 'Okta' | 'GenericSaml';
}

interface AuthState {
  // State
  user: User | null;
  tenant: Tenant | null;
  accessToken: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;

  // Actions
  setUser: (user: User, tenant: Tenant, accessToken: string) => void;
  setAccessToken: (token: string) => void;
  clearAuth: () => void;
  setLoading: (loading: boolean) => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      // Initial state
      user: null,
      tenant: null,
      accessToken: null,
      isAuthenticated: false,
      isLoading: false,

      // Actions
      setUser: (user, tenant, accessToken) => {
        set({
          user,
          tenant,
          accessToken,
          isAuthenticated: true,
          isLoading: false,
        });
      },

      setAccessToken: (accessToken) => {
        set({ accessToken });
      },

      clearAuth: () => {
        set({
          user: null,
          tenant: null,
          accessToken: null,
          isAuthenticated: false,
          isLoading: false,
        });
      },

      setLoading: (isLoading) => {
        set({ isLoading });
      },
    }),
    {
      name: 'colaflow-auth', // Key in storage
      storage: createJSONStorage(() => sessionStorage), // Use sessionStorage
      partialize: (state) => ({
        // Only persist user and tenant info, NOT the token
        user: state.user,
        tenant: state.tenant,
        // Do NOT persist accessToken (security)
      }),
    }
  )
);

// Selectors (for performance optimization)
export const useUser = () => useAuthStore((state) => state.user);
export const useTenant = () => useAuthStore((state) => state.tenant);
export const useIsAuthenticated = () => useAuthStore((state) => state.isAuthenticated);
```

**Usage**:

```tsx
// In a component
import { useAuthStore, useUser, useTenant } from '@/stores/useAuthStore';

function UserProfile() {
  // Method 1: Select specific fields (recommended for performance)
  const user = useUser();
  const tenant = useTenant();

  // Method 2: Select multiple fields
  const { user, tenant } = useAuthStore((state) => ({
    user: state.user,
    tenant: state.tenant,
  }));

  // Method 3: Access actions
  const clearAuth = useAuthStore((state) => state.clearAuth);

  return (
    <div>
      <h1>{user?.fullName}</h1>
      <p>{tenant?.name}</p>
      <button onClick={clearAuth}>Logout</button>
    </div>
  );
}
```

---

### 2. UI Store

**File**: `stores/useUiStore.ts`

```typescript
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

interface UiState {
  // Sidebar
  sidebarOpen: boolean;
  toggleSidebar: () => void;
  setSidebarOpen: (open: boolean) => void;

  // Theme
  theme: 'light' | 'dark' | 'system';
  setTheme: (theme: 'light' | 'dark' | 'system') => void;

  // Language
  language: 'en' | 'zh-CN';
  setLanguage: (language: 'en' | 'zh-CN') => void;

  // Notifications
  notificationCount: number;
  setNotificationCount: (count: number) => void;
}

export const useUiStore = create<UiState>()(
  persist(
    (set) => ({
      // Sidebar
      sidebarOpen: true,
      toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
      setSidebarOpen: (open) => set({ sidebarOpen: open }),

      // Theme
      theme: 'system',
      setTheme: (theme) => set({ theme }),

      // Language
      language: 'en',
      setLanguage: (language) => set({ language }),

      // Notifications
      notificationCount: 0,
      setNotificationCount: (notificationCount) => set({ notificationCount }),
    }),
    {
      name: 'colaflow-ui',
      storage: createJSONStorage(() => localStorage), // Persist UI preferences
    }
  )
);

// Selectors
export const useSidebarOpen = () => useUiStore((state) => state.sidebarOpen);
export const useTheme = () => useUiStore((state) => state.theme);
```

---

## TanStack Query Setup

### Query Client Configuration

**File**: `lib/query-client.ts`

```typescript
import { QueryClient, DefaultOptions } from '@tanstack/react-query';

const queryConfig: DefaultOptions = {
  queries: {
    // Stale time: Data is considered fresh for 5 minutes
    staleTime: 1000 * 60 * 5,

    // Garbage collection time: Unused data is garbage collected after 10 minutes
    gcTime: 1000 * 60 * 10, // Renamed from cacheTime in v5

    // Refetch on window focus
    refetchOnWindowFocus: false,

    // Retry failed requests
    retry: 1,

    // Retry delay
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
  },
  mutations: {
    // Retry failed mutations
    retry: 0,
  },
};

export const queryClient = new QueryClient({
  defaultOptions: queryConfig,
});
```

**Provider Setup**:

**File**: `app/layout.tsx`

```typescript
'use client';

import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from '@/lib/query-client';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <QueryClientProvider client={queryClient}>
          {children}

          {/* DevTools (only in development) */}
          {process.env.NODE_ENV === 'development' && (
            <ReactQueryDevtools initialIsOpen={false} />
          )}
        </QueryClientProvider>
      </body>
    </html>
  );
}
```

---

## Custom Hooks

### Authentication Hooks

#### useLogin

**File**: `hooks/auth/useLogin.ts`

```typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authService } from '@/services/auth.service';
import { useAuthStore } from '@/stores/useAuthStore';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';

export function useLogin() {
  const setUser = useAuthStore((state) => state.setUser);
  const router = useRouter();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: authService.login,

    onSuccess: (data) => {
      // Store user, tenant, and token in Zustand
      setUser(data.user, data.tenant, data.accessToken);

      // Show success message
      toast.success(`Welcome back, ${data.user.fullName}!`);

      // Prefetch user data (optional)
      queryClient.prefetchQuery({
        queryKey: ['user', data.user.id],
        queryFn: () => authService.getCurrentUser(),
      });

      // Redirect to dashboard or original page
      const redirectUrl = sessionStorage.getItem('redirect_after_login') || '/dashboard';
      sessionStorage.removeItem('redirect_after_login');
      router.push(redirectUrl);
    },

    onError: (error: any) => {
      // Show error message
      const message = error.response?.data?.message || 'Login failed. Please try again.';
      toast.error(message);
    },
  });
}
```

**Usage**:

```tsx
// In a login component
import { useLogin } from '@/hooks/auth/useLogin';

function LoginForm() {
  const login = useLogin();

  const handleSubmit = (data: { email: string; password: string }) => {
    login.mutate(data);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <button type="submit" disabled={login.isPending}>
        {login.isPending ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}
```

---

#### useLogout

**File**: `hooks/auth/useLogout.ts`

```typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authService } from '@/services/auth.service';
import { useAuthStore } from '@/stores/useAuthStore';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';

export function useLogout() {
  const clearAuth = useAuthStore((state) => state.clearAuth);
  const router = useRouter();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: authService.logout,

    onSuccess: () => {
      // Clear auth state
      clearAuth();

      // Clear all query cache
      queryClient.clear();

      // Show success message
      toast.success('Logged out successfully');

      // Redirect to login page
      router.push('/login');
    },

    onError: () => {
      // Even if logout fails on backend, clear client state
      clearAuth();
      queryClient.clear();
      router.push('/login');
    },
  });
}
```

---

### Tenant Hooks

#### useCheckSlug

**File**: `hooks/tenants/useCheckSlug.ts`

```typescript
import { useQuery } from '@tanstack/react-query';
import { tenantService } from '@/services/tenant.service';
import { useMemo } from 'react';

export function useCheckSlug(slug: string, enabled: boolean = true) {
  // Trim and lowercase
  const normalizedSlug = slug.trim().toLowerCase();

  return useQuery({
    queryKey: ['check-slug', normalizedSlug],
    queryFn: () => tenantService.checkSlugAvailability(normalizedSlug),
    enabled: enabled && normalizedSlug.length >= 3,
    staleTime: 5000, // Cache for 5 seconds
    retry: false, // Don't retry slug checks
  });
}
```

**Usage**:

```tsx
import { useCheckSlug } from '@/hooks/tenants/useCheckSlug';

function TenantSlugInput() {
  const [slug, setSlug] = useState('');
  const { data, isLoading, error } = useCheckSlug(slug, slug.length >= 3);

  return (
    <div>
      <input
        value={slug}
        onChange={(e) => setSlug(e.target.value)}
        placeholder="your-company"
      />

      {isLoading && <span>Checking...</span>}

      {data?.available === true && (
        <span className="text-green-600">✓ Available</span>
      )}

      {data?.available === false && (
        <span className="text-red-600">✗ Taken</span>
      )}
    </div>
  );
}
```

---

#### useSsoConfig

**File**: `hooks/tenants/useSsoConfig.ts`

```typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { tenantService } from '@/services/tenant.service';
import { useTenant } from '@/stores/useAuthStore';
import { toast } from 'sonner';

// Get SSO config
export function useSsoConfig() {
  const tenant = useTenant();

  return useQuery({
    queryKey: ['sso-config', tenant?.id],
    queryFn: () => tenantService.getSsoConfig(tenant!.id),
    enabled: !!tenant,
    staleTime: 1000 * 60 * 10, // 10 minutes
  });
}

// Update SSO config
export function useUpdateSsoConfig() {
  const tenant = useTenant();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (config: any) => tenantService.updateSsoConfig(tenant!.id, config),

    onSuccess: () => {
      // Invalidate SSO config query
      queryClient.invalidateQueries({ queryKey: ['sso-config', tenant?.id] });

      toast.success('SSO configuration updated successfully');
    },

    onError: (error: any) => {
      const message = error.response?.data?.message || 'Failed to update SSO configuration';
      toast.error(message);
    },
  });
}

// Test SSO connection
export function useTestSsoConnection() {
  const tenant = useTenant();

  return useMutation({
    mutationFn: () => tenantService.testSsoConnection(tenant!.id),

    onSuccess: (data) => {
      if (data.success) {
        toast.success('SSO connection successful!');
      } else {
        toast.error(`Connection failed: ${data.message}`);
      }
    },

    onError: () => {
      toast.error('Failed to test SSO connection');
    },
  });
}
```

---

### MCP Token Hooks

#### useMcpTokens

**File**: `hooks/mcp/useMcpTokens.ts`

```typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { mcpService } from '@/services/mcp.service';
import { toast } from 'sonner';

// List all tokens
export function useMcpTokens() {
  return useQuery({
    queryKey: ['mcp-tokens'],
    queryFn: mcpService.listTokens,
    staleTime: 1000 * 60 * 5, // 5 minutes
  });
}

// Get single token details
export function useMcpToken(tokenId: string) {
  return useQuery({
    queryKey: ['mcp-token', tokenId],
    queryFn: () => mcpService.getToken(tokenId),
    enabled: !!tokenId,
  });
}

// Create new token
export function useCreateMcpToken() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: mcpService.createToken,

    onSuccess: () => {
      // Invalidate tokens list
      queryClient.invalidateQueries({ queryKey: ['mcp-tokens'] });

      toast.success('MCP Token created successfully');
    },

    onError: (error: any) => {
      const message = error.response?.data?.message || 'Failed to create token';
      toast.error(message);
    },
  });
}

// Revoke token
export function useRevokeMcpToken() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ tokenId, reason }: { tokenId: string; reason?: string }) =>
      mcpService.revokeToken(tokenId, reason),

    onSuccess: () => {
      // Invalidate tokens list
      queryClient.invalidateQueries({ queryKey: ['mcp-tokens'] });

      toast.success('Token revoked successfully');
    },

    onError: () => {
      toast.error('Failed to revoke token');
    },
  });
}

// Get audit logs
export function useMcpAuditLogs(tokenId: string, filters?: any) {
  return useQuery({
    queryKey: ['mcp-audit-logs', tokenId, filters],
    queryFn: () => mcpService.getAuditLogs(tokenId, filters),
    enabled: !!tokenId,
    staleTime: 1000 * 60, // 1 minute
  });
}
```

---

## State Synchronization

### Keeping AuthStore and Query Cache in Sync

**Problem**: When user logs out, TanStack Query cache may still contain user-specific data.

**Solution**: Clear query cache on logout.

```typescript
// hooks/auth/useLogout.ts
export function useLogout() {
  const clearAuth = useAuthStore((state) => state.clearAuth);
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: authService.logout,

    onSuccess: () => {
      // 1. Clear auth state
      clearAuth();

      // 2. Clear ALL query cache
      queryClient.clear();

      // Or selectively clear specific queries:
      // queryClient.removeQueries({ queryKey: ['projects'] });
      // queryClient.removeQueries({ queryKey: ['issues'] });

      // 3. Redirect
      router.push('/login');
    },
  });
}
```

---

### Optimistic Updates

**Use Case**: Immediately update UI before API call completes.

**Example**: Revoke MCP token

```typescript
// hooks/mcp/useMcpTokens.ts
export function useRevokeMcpToken() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ tokenId, reason }: { tokenId: string; reason?: string }) =>
      mcpService.revokeToken(tokenId, reason),

    // Optimistic update
    onMutate: async ({ tokenId }) => {
      // Cancel any outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['mcp-tokens'] });

      // Snapshot the previous value
      const previousTokens = queryClient.getQueryData(['mcp-tokens']);

      // Optimistically update to the new value
      queryClient.setQueryData(['mcp-tokens'], (old: any) => ({
        ...old,
        tokens: old?.tokens.map((token: any) =>
          token.id === tokenId ? { ...token, status: 'Revoked' } : token
        ),
      }));

      // Return context with previous value
      return { previousTokens };
    },

    // On error, roll back to previous value
    onError: (_err, _variables, context) => {
      if (context?.previousTokens) {
        queryClient.setQueryData(['mcp-tokens'], context.previousTokens);
      }

      toast.error('Failed to revoke token');
    },

    // Always refetch after success or error
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['mcp-tokens'] });
    },
  });
}
```

---

## Best Practices

### 1. Use Query Keys Consistently

**Good**:
```typescript
// All related queries use the same base key
queryKey: ['mcp-tokens']
queryKey: ['mcp-token', tokenId]
queryKey: ['mcp-audit-logs', tokenId]

// Invalidate all MCP token queries:
queryClient.invalidateQueries({ queryKey: ['mcp-tokens'] });
queryClient.invalidateQueries({ queryKey: ['mcp-token'] });
```

**Bad**:
```typescript
// Inconsistent keys
queryKey: ['tokens']
queryKey: ['mcpToken', tokenId]
queryKey: ['audit', tokenId]
```

---

### 2. Use Selectors for Performance

**Good** (only re-renders when `user` changes):
```tsx
const user = useAuthStore((state) => state.user);
```

**Bad** (re-renders when ANY auth state changes):
```tsx
const { user } = useAuthStore();
```

**Even Better** (dedicated selector):
```tsx
// stores/useAuthStore.ts
export const useUser = () => useAuthStore((state) => state.user);

// In component
const user = useUser();
```

---

### 3. Prefetch Critical Data

```typescript
// app/(dashboard)/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  const queryClient = useQueryClient();
  const user = useUser();

  useEffect(() => {
    if (user) {
      // Prefetch projects
      queryClient.prefetchQuery({
        queryKey: ['projects'],
        queryFn: projectService.getAll,
      });

      // Prefetch issues
      queryClient.prefetchQuery({
        queryKey: ['issues'],
        queryFn: issueService.getAll,
      });
    }
  }, [user, queryClient]);

  return <div>{children}</div>;
}
```

---

### 4. Handle Loading and Error States

```tsx
function ProjectList() {
  const { data, isLoading, error } = useProjects();

  if (isLoading) {
    return <LoadingSpinner />;
  }

  if (error) {
    return (
      <ErrorMessage
        message="Failed to load projects"
        retry={() => refetch()}
      />
    );
  }

  if (!data || data.length === 0) {
    return <EmptyState message="No projects yet" />;
  }

  return (
    <div>
      {data.map((project) => (
        <ProjectCard key={project.id} project={project} />
      ))}
    </div>
  );
}
```

---

### 5. Use Mutations for Write Operations

**Always use mutations** (not queries) for creating, updating, deleting data:

```typescript
// ✅ Good
const createProject = useMutation({
  mutationFn: projectService.create,
});

// ❌ Bad (don't use query for mutations)
const createProject = useQuery({
  queryKey: ['create-project'],
  queryFn: projectService.create,
});
```

---

## Performance Optimization

### 1. Query Deduplication

TanStack Query automatically deduplicates identical queries:

```tsx
// Both components will share the same query
function ComponentA() {
  const { data } = useMcpTokens();
  // ...
}

function ComponentB() {
  const { data } = useMcpTokens(); // Same query, shared data
  // ...
}
```

---

### 2. Background Refetching

Keep data fresh without blocking UI:

```typescript
export function useMcpTokens() {
  return useQuery({
    queryKey: ['mcp-tokens'],
    queryFn: mcpService.listTokens,
    staleTime: 1000 * 60 * 5, // 5 minutes

    // Refetch in background when data is stale
    refetchOnMount: true,
    refetchOnReconnect: true,
    refetchOnWindowFocus: false, // Disable for better UX
  });
}
```

---

### 3. Pagination

```typescript
export function useMcpAuditLogs(tokenId: string, page: number, limit: number) {
  return useQuery({
    queryKey: ['mcp-audit-logs', tokenId, page, limit],
    queryFn: () => mcpService.getAuditLogs(tokenId, { page, limit }),
    enabled: !!tokenId,

    // Keep previous data while fetching next page
    placeholderData: (previousData) => previousData,
  });
}
```

**Usage**:
```tsx
function AuditLogTable({ tokenId }: { tokenId: string }) {
  const [page, setPage] = useState(1);
  const { data, isLoading, isPlaceholderData } = useMcpAuditLogs(tokenId, page, 50);

  return (
    <div>
      <Table data={data?.logs} loading={isPlaceholderData} />

      <Pagination
        currentPage={page}
        totalPages={data?.pagination.totalPages}
        onPageChange={setPage}
      />
    </div>
  );
}
```

---

### 4. Infinite Queries

For infinite scroll:

```typescript
import { useInfiniteQuery } from '@tanstack/react-query';

export function useInfiniteAuditLogs(tokenId: string) {
  return useInfiniteQuery({
    queryKey: ['mcp-audit-logs-infinite', tokenId],
    queryFn: ({ pageParam = 1 }) =>
      mcpService.getAuditLogs(tokenId, { page: pageParam, limit: 50 }),
    getNextPageParam: (lastPage) => {
      const { page, totalPages } = lastPage.pagination;
      return page < totalPages ? page + 1 : undefined;
    },
    enabled: !!tokenId,
  });
}
```

**Usage**:
```tsx
function InfiniteAuditLogList({ tokenId }: { tokenId: string }) {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteAuditLogs(tokenId);

  return (
    <div>
      {data?.pages.map((page) =>
        page.logs.map((log) => <AuditLogItem key={log.id} log={log} />)
      )}

      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}
```

---

## Conclusion

This state management guide demonstrates how to effectively combine Zustand and TanStack Query in ColaFlow's frontend:

**Key Takeaways**:
- ✅ Use Zustand for authentication and UI state
- ✅ Use TanStack Query for all server data
- ✅ Clear query cache on logout
- ✅ Use optimistic updates for instant UX
- ✅ Use selectors for performance
- ✅ Prefetch critical data
- ✅ Handle loading/error states gracefully
- ✅ Use mutations for write operations

**Next Document**: `component-library.md` (Reusable components catalog)
