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 provider1. 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 production2. 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
}