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 minServer 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 minLocal 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 minDerived 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 minutesServer State vs Client State
Local vs Global State
Derived State Patterns
State Normalization
2
Server State Management
25 minutesReact Query / TanStack Query
SWR Implementation
Caching Strategies
Background Updates
3
Client State Patterns
20 minutesZustand State Store
Context + Reducer Pattern
Local Storage Sync
State Persistence
4
Real-time State Updates
15 minutesWebSocket 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