Intermediate Tutorial

State Management with Next.js

Implement robust state management patterns for scalable applications

75 min
Total Duration
Intermediate
Difficulty Level
4
Key Modules

Step-by-Step Implementation

Tutorial Sections

1

Understanding State Types

Learn to identify and categorize different types of state in modern applications

1

Server State vs Client State

5 min

Server State - Data that lives on the server and is fetched by your application:
- User profiles, posts, comments
- Shopping cart contents from database
- Real-time data like stock prices
- Authentication status

Client State - Data that only exists in the browser:
- Form input values
- UI state (modals open/closed)
- Current page/route
- Theme preferences

Key Principle: Server state is shared and persistent, client state is local and temporary.
Implementation
// Example: Identifying state types in a user dashboard
interface ServerState {
  user: User;           // From API
  posts: Post[];        // From database
  notifications: Notification[];  // Real-time data
}

interface ClientState {
  sidebarOpen: boolean;    // UI state
  currentTab: string;      // Navigation state  
  formData: FormValues;    // Temporary form state
  theme: 'light' | 'dark'; // User preference
}
2

Local vs Global State

5 min

Local State - State that only affects a single component:
- Form field values
- Hover states
- Component-specific loading states

Global State - State that affects multiple components:
- User authentication
- Shopping cart
- App-wide settings
- Notifications

Decision Framework:
- If only one component needs it → Local state
- If multiple components need it → Global state
- If it persists across page changes → Global state
Implementation
// Local state example
function ContactForm() {
  const [name, setName] = useState(''); // Local - only this form needs it
  const [email, setEmail] = useState('');
  
  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
    </form>
  );
}

// Global state example  
const useAuthStore = create((set) => ({
  user: null,              // Global - used throughout app
  login: (user) => set({ user }),
  logout: () => set({ user: null })
}));
3

Derived State & Normalization

5 min

Derived State - Values calculated from existing state:
- Filtered lists
- Computed totals
- Formatted dates
- Validation status

State Normalization - Organizing state to avoid duplication:
- Store entities by ID in objects
- Use arrays of IDs for ordering
- Avoid nested duplicates

Best Practice: Don't store what you can calculate!
Implementation
// Derived state example
function ProductList({ products, searchTerm }) {
  // Derived - calculated from props
  const filteredProducts = useMemo(() => 
    products.filter(p => 
      p.name.toLowerCase().includes(searchTerm.toLowerCase())
    ), [products, searchTerm]
  );
  
  const totalPrice = useMemo(() =>
    filteredProducts.reduce((sum, p) => sum + p.price, 0),
    [filteredProducts]
  );
}

// Normalized state structure
interface NormalizedState {
  entities: {
    users: Record<string, User>;
    posts: Record<string, Post>;
  };
  ids: {
    users: string[];
    posts: string[];
  };
}
Section 1 of 4

Tutorial Modules

1

Understanding State Types

15 minutes
Server State vs Client State
Local vs Global State
Derived State Patterns
State Normalization
2

Server State Management

25 minutes
React Query / TanStack Query
SWR Implementation
Caching Strategies
Background Updates
3

Client State Patterns

20 minutes
Zustand State Store
Context + Reducer Pattern
Local Storage Sync
State Persistence
4

Real-time State Updates

15 minutes
WebSocket Integration
Server-Sent Events
Optimistic Updates
Conflict Resolution

State Management Patterns

Server State

Remote data fetching and caching

Automatic refetching
Background updates
Error retry logic
Optimistic updates

Client State

Local application state management

Global state store
Component isolation
State persistence
Developer tools

Form State

Complex form data handling

Validation logic
Field dependencies
Async validation
Error handling

UI State

Interface state and interactions

Modal management
Toast notifications
Loading states
Theme preferences

Code Examples

Server State with TanStack Query

// hooks/useProjects.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

interface Project {
  id: string;
  name: string;
  status: 'active' | 'completed' | 'paused';
}

export function useProjects() {
  return useQuery({
    queryKey: ['projects'],
    queryFn: async (): Promise<Project[]> => {
      const response = await fetch('/api/projects');
      if (!response.ok) throw new Error('Failed to fetch projects');
      return response.json();
    },
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
  });
}

export function useCreateProject() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: async (newProject: Omit<Project, 'id'>) => {
      const response = await fetch('/api/projects', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newProject),
      });
      return response.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['projects'] });
    },
  });
}

Client State with Zustand

// store/useAppStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface AppState {
  user: User | null;
  theme: 'light' | 'dark';
  sidebar: {
    isOpen: boolean;
    activeTab: string;
  };
  notifications: Notification[];
}

interface AppActions {
  setUser: (user: User | null) => void;
  toggleTheme: () => void;
  setSidebarOpen: (isOpen: boolean) => void;
  setActiveTab: (tab: string) => void;
  addNotification: (notification: Notification) => void;
  removeNotification: (id: string) => void;
}

export const useAppStore = create<AppState & AppActions>()(
  persist(
    (set, get) => ({
      // State
      user: null,
      theme: 'light',
      sidebar: {
        isOpen: true,
        activeTab: 'dashboard',
      },
      notifications: [],

      // Actions
      setUser: (user) => set({ user }),
      toggleTheme: () => set((state) => ({ 
        theme: state.theme === 'light' ? 'dark' : 'light' 
      })),
      setSidebarOpen: (isOpen) => set((state) => ({
        sidebar: { ...state.sidebar, isOpen }
      })),
      setActiveTab: (activeTab) => set((state) => ({
        sidebar: { ...state.sidebar, activeTab }
      })),
      addNotification: (notification) => set((state) => ({
        notifications: [...state.notifications, notification]
      })),
      removeNotification: (id) => set((state) => ({
        notifications: state.notifications.filter(n => n.id !== id)
      })),
    }),
    {
      name: 'app-store',
      partialize: (state) => ({ 
        theme: state.theme, 
        sidebar: state.sidebar 
      }),
    }
  )
);

Form State Management

// components/ProjectForm.tsx
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const projectSchema = z.object({
  name: z.string().min(1, 'Project name is required'),
  description: z.string().optional(),
  status: z.enum(['active', 'completed', 'paused']),
  tags: z.array(z.string()),
  dueDate: z.date().optional(),
});

type ProjectFormData = z.infer<typeof projectSchema>;

export function ProjectForm({ 
  initialData, 
  onSubmit 
}: {
  initialData?: Partial<ProjectFormData>;
  onSubmit: (data: ProjectFormData) => Promise<void>;
}) {
  const {
    control,
    handleSubmit,
    formState: { errors, isSubmitting, isDirty },
    watch,
    setValue,
  } = useForm<ProjectFormData>({
    resolver: zodResolver(projectSchema),
    defaultValues: initialData,
  });

  const watchedStatus = watch('status');

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
      <Controller
        name="name"
        control={control}
        render={({ field }) => (
          <div>
            <label className="block text-sm font-medium mb-2">
              Project Name
            </label>
            <input
              {...field}
              className="w-full px-3 py-2 border rounded-lg"
              placeholder="Enter project name"
            />
            {errors.name && (
              <p className="text-red-500 text-sm mt-1">
                {errors.name.message}
              </p>
            )}
          </div>
        )}
      />

      <Controller
        name="status"
        control={control}
        render={({ field }) => (
          <div>
            <label className="block text-sm font-medium mb-2">
              Status
            </label>
            <select {...field} className="w-full px-3 py-2 border rounded-lg">
              <option value="active">Active</option>
              <option value="completed">Completed</option>
              <option value="paused">Paused</option>
            </select>
          </div>
        )}
      />

      {watchedStatus === 'completed' && (
        <Controller
          name="dueDate"
          control={control}
          render={({ field }) => (
            <div>
              <label className="block text-sm font-medium mb-2">
                Completion Date
              </label>
              <input
                {...field}
                type="date"
                className="w-full px-3 py-2 border rounded-lg"
              />
            </div>
          )}
        />
      )}

      <button
        type="submit"
        disabled={isSubmitting || !isDirty}
        className="px-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50"
      >
        {isSubmitting ? 'Saving...' : 'Save Project'}
      </button>
    </form>
  );
}

Real-time State Updates

// hooks/useRealTimeUpdates.ts
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useAppStore } from '../store/useAppStore';

export function useRealTimeUpdates() {
  const queryClient = useQueryClient();
  const { user, addNotification } = useAppStore();

  useEffect(() => {
    if (!user) return;

    // WebSocket connection
    const ws = new WebSocket(`${process.env.NEXT_PUBLIC_WS_URL}/updates`);

    ws.onopen = () => {
      console.log('Connected to real-time updates');
      // Authenticate the connection
      ws.send(JSON.stringify({
        type: 'auth',
        token: user.token,
      }));
    };

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);

      switch (message.type) {
        case 'project_updated':
          // Invalidate projects query to refetch
          queryClient.invalidateQueries({ queryKey: ['projects'] });
          
          // Show notification
          addNotification({
            id: Date.now().toString(),
            type: 'info',
            message: `Project "${message.data.name}" was updated`,
            timestamp: new Date(),
          });
          break;

        case 'user_mentioned':
          // Update specific query data
          queryClient.setQueryData(['notifications'], (old: any) => [
            ...old,
            message.data,
          ]);
          
          addNotification({
            id: Date.now().toString(),
            type: 'mention',
            message: `You were mentioned in "${message.data.context}"`,
            timestamp: new Date(),
          });
          break;

        case 'system_update':
          // Handle system-wide updates
          queryClient.invalidateQueries();
          break;
      }
    };

    ws.onclose = () => {
      console.log('Disconnected from real-time updates');
    };

    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      addNotification({
        id: Date.now().toString(),
        type: 'error',
        message: 'Lost connection to real-time updates',
        timestamp: new Date(),
      });
    };

    return () => {
      ws.close();
    };
  }, [user, queryClient, addNotification]);
}

Best Practices

Separation of Concerns

Keep server state and client state separate

Use React Query for server state
Use Zustand/Context for client state
Avoid mixing state types
Clear data ownership

Performance Optimization

Optimize state updates and re-renders

Memoize expensive calculations
Use selective subscriptions
Implement proper caching
Debounce frequent updates

Error Handling

Robust error handling and recovery

Implement retry logic
Provide fallback states
Show meaningful error messages
Log errors for debugging

Developer Experience

Tools and patterns for better DX

Use TypeScript for type safety
Implement devtools integration
Create custom hooks
Document state patterns

Master State Management

Build scalable applications with robust state patterns