State Management

Scalable architecture for application state management using Zustand (client state) and TanStack Query (server state).

Architecture

Folder structure for state management in each application.

lib/
├── config/
│   └── env.ts              # Environment configuration
├── api/
│   ├── client.ts           # Base API client
│   ├── types.ts            # API types
│   └── services/
│       ├── users.service.ts
│       ├── billing.service.ts
│       └── security.service.ts
├── stores/
│   ├── auth.store.ts       # Zustand auth store
│   └── ui.store.ts         # Zustand UI store
├── queries/
│   ├── keys.ts             # Query key factory
│   ├── users.queries.ts
│   ├── billing.queries.ts
│   └── security.queries.ts
├── hooks/
│   ├── useAuth.ts          # Auth integration hook
│   ├── useBilling.ts
│   └── useSecurity.ts
└── providers/
    └── QueryProvider.tsx   # TanStack Query provider

1. Environment Config

Type-safe environment variable configuration with default values.

lib/config/env.ts

import { env, API_BASE_URL, APP_BASE_URL } from '@/lib/config/env'

// Usage
console.log(API_BASE_URL)        // http://0.0.0.0:8000 (default)
console.log(env.isDev)           // true in development
console.log(env.isProd)          // true in production

2. Zustand Stores

Lightweight stores for client state without middleware.

Auth Store

import { useAuthStore } from '@/lib/stores'

function MyComponent() {
  // Get state
  const token = useAuthStore((state) => state.token)
  const user = useAuthStore((state) => state.user)
  const isAuthenticated = useAuthStore((state) => state.isAuthenticated)

  // Actions
  const { setToken, setUser, logout, hydrate } = useAuthStore()

  // Hydrate on mount (restore from localStorage)
  useEffect(() => {
    hydrate()
  }, [])

  // Set token (automatically saved to localStorage)
  setToken('your-jwt-token')

  // Logout (clears localStorage and state)
  logout()
}

UI Store

import { useUIStore } from '@/lib/stores'

function Sidebar() {
  const sidebarOpen = useUIStore((state) => state.sidebarOpen)
  const toggleSidebar = useUIStore((state) => state.toggleSidebar)

  return (
    <aside className={sidebarOpen ? 'w-64' : 'w-0'}>
      <button onClick={toggleSidebar}>Toggle</button>
    </aside>
  )
}

function Modal() {
  const activeModal = useUIStore((state) => state.activeModal)
  const { openModal, closeModal } = useUIStore()

  return (
    <>
      <button onClick={() => openModal('confirm-delete')}>
        Delete
      </button>
      {activeModal === 'confirm-delete' && (
        <div className="modal">
          <button onClick={closeModal}>Close</button>
        </div>
      )}
    </>
  )
}

3. TanStack Query

Server state management with caching, automatic refetch, and optimistic updates.

Query Keys

import { queryKeys } from '@/lib/queries'

// Usage in queries/mutations
queryKeys.users.all          // ['users']
queryKeys.users.me()         // ['users', 'me']
queryKeys.billing.invoices() // ['billing', 'invoices']
queryKeys.security.tokens()  // ['security', 'tokens']
queryKeys.security.ipList()  // ['security', 'ip-list']

Users Queries

import {
  useCurrentUser,
  useLoginMutation,
  useLogoutMutation,
  useRegisterMutation,
  useUpdateProfileMutation,
} from '@/lib/queries'

function ProfilePage() {
  // Query: GET /api/users/me/
  const { data: user, isLoading, error } = useCurrentUser()

  // Mutation: PATCH /api/users/me/
  const updateProfile = useUpdateProfileMutation()

  const handleUpdate = async () => {
    await updateProfile.mutateAsync({ name: 'New Name' })
  }

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return <div>Hello, {user?.name}</div>
}

function LoginForm() {
  const login = useLoginMutation()

  const handleSubmit = async (data) => {
    try {
      await login.mutateAsync({
        email: data.email,
        password: data.password,
        totp_code: data.totpCode, // optional 2FA
      })
      // Token is automatically saved to AuthStore
    } catch (error) {
      console.error('Login failed:', error)
    }
  }
}

Billing Queries

import { useInvoices, useCreateCheckoutMutation } from '@/lib/queries'

function BillingPage() {
  // Query: GET /api/billing/invoices/
  const { data: invoices, isLoading } = useInvoices()

  // Mutation: POST /api/billing/stripe/checkout/
  const createCheckout = useCreateCheckoutMutation()

  const handleUpgrade = async () => {
    await createCheckout.mutateAsync({
      subscription: 'pro',
      interval: 'month',
    })
    // Automatically redirects to Stripe checkout
  }

  return (
    <div>
      <h1>Invoices</h1>
      {invoices?.map((invoice) => (
        <div key={invoice.id}>
          {invoice.number} - {invoice.amount_paid}
        </div>
      ))}
      <button onClick={handleUpgrade}>Upgrade to Pro</button>
    </div>
  )
}

Security Queries

import {
  useApiTokens,
  useCreateApiTokenMutation,
  useDeleteApiTokenMutation,
  useIpList,
  useAddIpMutation,
} from '@/lib/queries'

function SecuritySettings() {
  const { data: tokens } = useApiTokens()
  const { data: ipList } = useIpList()
  const createToken = useCreateApiTokenMutation()
  const addIp = useAddIpMutation()

  const handleCreateToken = async () => {
    const newToken = await createToken.mutateAsync('My API Token')
    // Cache is automatically invalidated
  }

  const handleAddIp = async () => {
    await addIp.mutateAsync('192.168.1.1')
  }
}

4. Integration Hooks

High-level hooks combining Zustand stores and TanStack Query.

useAuth

import { useAuth } from '@/lib/hooks/useAuth'

function App() {
  const {
    user,              // User | null
    token,             // string | null
    isAuthenticated,   // boolean
    isLoading,         // boolean
    login,             // (data: LoginRequest) => Promise
    logout,            // () => Promise
    register,          // (data: RegisterRequest) => Promise
    updateProfile,     // (data: Partial<User>) => Promise
    loginStatus,       // 'idle' | 'pending' | 'success' | 'error'
  } = useAuth()

  if (isLoading) return <div>Loading...</div>

  if (!isAuthenticated) {
    return <LoginPage />
  }

  return <Dashboard user={user} />
}

useBilling

import { useBilling } from '@/lib/hooks/useBilling'

function BillingSection() {
  const {
    invoices,        // Invoice[]
    isLoading,       // boolean
    error,           // Error | null
    createCheckout,  // (data: CheckoutRequest) => Promise
    checkoutStatus,  // 'idle' | 'pending' | 'success' | 'error'
  } = useBilling()
}

useSecurity

import { useSecurity } from '@/lib/hooks/useSecurity'

function SecuritySection() {
  const {
    apiTokens,       // ApiToken[]
    tokensLoading,   // boolean
    createToken,     // (name: string) => Promise
    deleteToken,     // (tokenId: string) => Promise
    ipList,          // IPAllowlist[]
    ipListLoading,   // boolean
    addIp,           // (ipAddr: string) => Promise
    removeIp,        // (ipId: string) => Promise
  } = useSecurity()
}

5. API Services

Low-level services for direct API calls.

import { usersService, billingService, securityService } from '@/lib/api/services'

// Users API
await usersService.login({ email, password })
await usersService.register({ name, email, password })
await usersService.getMe(token)
await usersService.updateMe(token, { name: 'New Name' })
await usersService.changePassword(token, { old_password, new_password })
await usersService.setup2FA(token)
await usersService.verify2FA(token, totpCode)

// Billing API
await billingService.getInvoices(token)
await billingService.createCheckout(token, { subscription: 'pro' })

// Security API
await securityService.getApiTokens(token)
await securityService.createApiToken(token, 'My Token')
await securityService.getIpList(token)
await securityService.addIp(token, '192.168.1.1')

6. Setup

Application integration.

app/providers.tsx

'use client'

import { QueryProvider } from '@/lib/providers/QueryProvider'
import type { ReactNode } from 'react'

export function Providers({ children }: { children: ReactNode }) {
  return <QueryProvider>{children}</QueryProvider>
}

app/layout.tsx

import { Providers } from './providers'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  )
}

API Types

TypeScript types for API.

import type {
  User,
  Invoice,
  ApiToken,
  IPAllowlist,
  LoginRequest,
  LoginResponse,
  RegisterRequest,
  CheckoutRequest,
  CheckoutResponse,
  ApiResponse,
  PaginatedResponse,
} from '@/lib/api/types'

// User
type User = {
  id: string
  name: string
  email: string
  username: string
  is_active: boolean
  is_banned: boolean
  is_dark_mode: boolean
  credits: number
  generated_posts: number
}

// Invoice
type Invoice = {
  id: string
  number: string
  amount_paid: number
  date: string
}

// ApiToken
type ApiToken = {
  id: string
  name: string
  api_key: string
  created_at: string
}