Skip to content

State Management Guide

Comprehensive guide to managing state in Constellation.

State Management Strategy

Choose the right state management approach based on scope:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚         State Management Hierarchy                   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 1. Local Component State (useState)                  โ”‚
โ”‚    โ†“ Single component, no sharing needed            โ”‚
โ”‚                                                      โ”‚
โ”‚ 2. Custom Hooks (useHook)                           โ”‚
โ”‚    โ†“ Multiple components, reusable logic            โ”‚
โ”‚                                                      โ”‚
โ”‚ 3. Context API + Hooks                              โ”‚
โ”‚    โ†“ Global state, medium scope                     โ”‚
โ”‚                                                      โ”‚
โ”‚ 4. Server-side Caching (React Cache)                โ”‚
โ”‚    โ†“ Data fetching, expensive operations            โ”‚
โ”‚                                                      โ”‚
โ”‚ 5. External Solutions (React Query, Redux)          โ”‚
โ”‚    โ†“ Complex state, synchronization needs           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

1. Local Component State

Best for: Form inputs, UI toggles, temporary data

'use client';

import { useState } from 'react';

export function FormComponent() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
  });

  const handleChange = (field: string, value: string) => {
    setFormData(prev => ({
      ...prev,
      [field]: value,
    }));
  };

  return (
    <form>
      <input
        value={formData.name}
        onChange={(e) => handleChange('name', e.target.value)}
      />
    </form>
  );
}

When to Use

  • โœ… Form inputs
  • โœ… Modal open/close
  • โœ… Loading states
  • โœ… Temporary UI state
  • โœ… Single component only

2. Custom Hooks

Best for: Reusable stateful logic

// src/hooks/use-form.ts
import { useState, useCallback } from 'react';

interface FormState {
  [key: string]: string | number | boolean;
}

export function useForm<T extends FormState>(initialValues: T) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [touched, setTouched] = useState<Record<string, boolean>>({});

  const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setValues(prev => ({ ...prev, [name]: value }));
  }, []);

  const handleBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
  }, []);

  const resetForm = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    resetForm,
    setFieldValue: (field: string, value: any) => {
      setValues(prev => ({ ...prev, [field]: value }));
    },
  };
}

Usage:

export function LoginForm() {
  const form = useForm({ email: '', password: '' });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    // Submit form.values
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        value={form.values.email}
        onChange={form.handleChange}
      />
    </form>
  );
}

When to Use

  • โœ… Reusable logic
  • โœ… Used in multiple components
  • โœ… Complex state with side effects
  • โœ… Encapsulated logic

3. Context API

Best for: Global app state

Creating Context

// src/components/context/AuthContext.tsx
'use client';

import { createContext, useContext, useState, ReactNode } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
  updateUser: (user: User) => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  const login = async (email: string, password: string) => {
    setIsLoading(true);
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
      });
      const userData = await response.json();
      setUser(userData);
    } finally {
      setIsLoading(false);
    }
  };

  const logout = async () => {
    setUser(null);
    await fetch('/api/auth/logout', { method: 'POST' });
  };

  const updateUser = (newUser: User) => {
    setUser(newUser);
  };

  return (
    <AuthContext.Provider
      value={{
        user,
        isLoading,
        isAuthenticated: !!user,
        login,
        logout,
        updateUser,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

Using Context

// src/components/UserProfile.tsx
'use client';

import { useAuth } from '@/src/components/context/AuthContext';

export function UserProfile() {
  const { user, logout } = useAuth();

  if (!user) return null;

  return (
    <div>
      <p>Welcome, {user.name}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Provider Setup

// src/app/layout.tsx
import { AuthProvider } from '@/src/components/context/AuthContext';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        <AuthProvider>
          {children}
        </AuthProvider>
      </body>
    </html>
  );
}

When to Use

  • โœ… Global app state (auth, theme)
  • โœ… Data needed by many components
  • โœ… Configuration values
  • โœ… User preferences

4. Server-side Caching

Best for: Expensive data fetching

// src/lib/cache.ts
import { cache } from 'react';
import { apiClient } from '@/src/lib/api';

// Cache per request cycle
export const getUserData = cache(async (userId: string) => {
  console.log('Fetching user:', userId);
  return apiClient.get(`/users/${userId}`);
});

// Cache with revalidation
export const getWorkspaces = cache(async () => {
  return apiClient.get('/workspaces');
});

Usage:

// src/app/dashboard/page.tsx
import { getUserData } from '@/src/lib/cache';

export default async function Dashboard() {
  const user = await getUserData('current');

  return <div>Welcome, {user.name}</div>;
}

When to Use

  • โœ… Expensive database queries
  • โœ… API calls in Server Components
  • โœ… Data needed across multiple renders
  • โœ… Avoid N+1 queries

5. External State Management

For advanced needs, consider:

React Query

import { useQuery } from '@tanstack/react-query';

export function UserProfile() {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => apiClient.get(`/users/${userId}`),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });

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

  return <div>User: {user.name}</div>;
}

When to Use

  • โœ… Complex data fetching
  • โœ… Need caching & synchronization
  • โœ… Background updates needed
  • โœ… Mutations with side effects

Pattern Comparison

Pattern Scope Complexity Use Case
useState Local Simple Form inputs, UI state
Custom Hook Multiple components Medium Reusable logic
Context API Global Medium App state, theme
React Query Data fetching High API caching, sync
Redux Very large High Enterprise apps

Best Practices

โœ… Do's

  • โœ… Keep state as local as possible
  • โœ… Lift state only when needed
  • โœ… Use custom hooks for logic
  • โœ… Memoize context to prevent unnecessary renders
  • โœ… Use context for infrequently changing data
  • โœ… Combine multiple contexts if needed
  • โœ… Type your state with TypeScript

โŒ Don'ts

  • โŒ Don't put all state in global context
  • โŒ Don't update context too frequently
  • โŒ Don't use context for rapidly changing data
  • โŒ Don't forget to memoize context providers
  • โŒ Don't use deeply nested contexts
  • โŒ Don't update unrelated state together
  • โŒ Don't ignore TypeScript warnings

Performance Optimization

Memoized Context Provider

// Prevent unnecessary re-renders
export function OptimizedAuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const value = useMemo(() => ({
    user,
    setUser,
  }), [user]);

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

Split Contexts

// Instead of one large context with everything
<AuthContext.Provider>
  <ThemeContext.Provider>
    <UserPreferencesContext.Provider>
      {children}
    </UserPreferencesContext.Provider>
  </ThemeContext.Provider>
</AuthContext.Provider>

Selective Subscription

// Only subscribe to the parts you need
const { user } = useAuth();      // Just user
const { login } = useAuth();     // Just login
const { user, login } = useAuth(); // Multiple

Testing State

Testing Hooks

import { renderHook, act } from '@testing-library/react';
import { useForm } from '@/src/hooks/use-form';

it('should handle form changes', () => {
  const { result } = renderHook(() => useForm({ name: '' }));

  act(() => {
    result.current.setFieldValue('name', 'John');
  });

  expect(result.current.values.name).toBe('John');
});

Testing Context

import { render, screen } from '@testing-library/react';
import { AuthProvider } from '@/src/components/context/AuthContext';

it('should provide user data', () => {
  render(
    <AuthProvider>
      <UserProfile />
    </AuthProvider>
  );

  expect(screen.getByText(/Welcome/)).toBeInTheDocument();
});

Migration Guide

From Redux to Context API

If migrating from Redux:

  1. Create contexts for major stores
  2. Move reducers to custom hooks
  3. Test thoroughly
  4. Update components gradually
  5. Remove Redux dependencies

Debugging State

React DevTools

  1. Install React DevTools extension
  2. Open DevTools
  3. Go to Components tab
  4. Inspect component state
  5. View context values

State Logging

export function useDebugState<T>(state: T, label: string) {
  useEffect(() => {
    console.log(`${label}:`, state);
  }, [state, label]);
}

// Usage
useDebugState(user, 'User');


Last Updated: January 2026