| name | complex-state-management |
| description | Production patterns for managing complex application state in React without Redux, Zustand, or other state libraries. Includes multi-stage loading, command patterns, refs for performance, and parallel data fetching. Use when building complex UIs with interconnected states, need loading stages and progress tracking, or implementing command patterns. |
Complex State Management Without External Libraries
Production patterns for managing complex application state in React without Redux, Zustand, or other state libraries. Includes multi-stage loading, command patterns, refs for performance, and parallel data fetching.
When to use this skill
- Building complex UIs with many interconnected states
- Need loading stages and progress tracking
- Implementing command patterns for centralized control
- Managing real-time updates and background operations
- Want to avoid Redux/Zustand overhead
- Building video players, editors, or multi-step flows
- Need precise performance control with refs
Core Patterns
- Multi-Stage Loading States - Track progress through complex operations
- Command Pattern - Centralized playback/control commands
- Ref-Based Optimization - Avoid re-renders for frequently changing values
- Memoized Setters - Prevent unnecessary child re-renders
- Parallel State Updates - Batch related changes together
Implementation
Pattern 1: Multi-Stage Loading with Progress
'use client';
import { useState, useRef } from 'react';
import { AbortManager } from '@/lib/promise-utils';
type PageState = 'IDLE' | 'ANALYZING_NEW' | 'LOADING_CACHED' | 'ERROR';
type LoadingStage = 'fetching' | 'understanding' | 'generating' | 'processing' | null;
export function ComplexPage() {
// Page state machine
const [pageState, setPageState] = useState<PageState>('IDLE');
const [loadingStage, setLoadingStage] = useState<LoadingStage>(null);
const [error, setError] = useState<string>('');
// Progress tracking
const [generationStartTime, setGenerationStartTime] = useState<number | null>(null);
const [processingStartTime, setProcessingStartTime] = useState<number | null>(null);
// Cleanup manager
const abortManager = useRef(new AbortManager());
const handleAnalyze = async () => {
try {
// Stage 1: Fetching
setPageState('ANALYZING_NEW');
setLoadingStage('fetching');
const controller1 = abortManager.current.createController('fetch', 30000);
const data = await fetch('/api/data', { signal: controller1.signal })
.then(r => r.json());
// Stage 2: Understanding
setLoadingStage('understanding');
// Stage 3: Generating
setLoadingStage('generating');
setGenerationStartTime(Date.now());
const controller2 = abortManager.current.createController('generate', 60000);
const analysis = await fetch('/api/analyze', {
signal: controller2.signal,
method: 'POST',
body: JSON.stringify(data)
}).then(r => r.json());
// Stage 4: Processing
setLoadingStage('processing');
setProcessingStartTime(Date.now());
// Process results...
setPageState('IDLE');
setLoadingStage(null);
setGenerationStartTime(null);
} catch (error) {
setPageState('ERROR');
setError(error.message);
}
};
// Cleanup on unmount
useEffect(() => {
return () => abortManager.current.cleanup();
}, []);
return (
<div>
{loadingStage && (
<LoadingIndicator
stage={loadingStage}
elapsedTime={generationStartTime ? Date.now() - generationStartTime : 0}
/>
)}
</div>
);
}
Pattern 2: Command Pattern for Centralized Control
// Define command types
export type PlaybackCommandType = 'SEEK' | 'PLAY_TOPIC' | 'PLAY_SEGMENT' | 'PLAY' | 'PAUSE' | 'PLAY_ALL';
export interface PlaybackCommand {
type: PlaybackCommandType;
time?: number;
topic?: Topic;
segment?: Segment;
autoPlay?: boolean;
}
// Parent component
export function VideoAnalysisPage() {
const [playbackCommand, setPlaybackCommand] = useState<PlaybackCommand | null>(null);
const handleTopicClick = (topic: Topic) => {
setPlaybackCommand({
type: 'PLAY_TOPIC',
topic,
autoPlay: true
});
};
const handleSeek = (time: number) => {
setPlaybackCommand({
type: 'SEEK',
time
});
};
return (
<div>
<VideoPlayer
command={playbackCommand}
onCommandExecuted={() => setPlaybackCommand(null)}
/>
<TopicsList
topics={topics}
onTopicClick={handleTopicClick}
/>
</div>
);
}
// Child component
export function VideoPlayer({
command,
onCommandExecuted
}: {
command: PlaybackCommand | null;
onCommandExecuted: () => void;
}) {
const playerRef = useRef<YouTubePlayer>(null);
useEffect(() => {
if (!command || !playerRef.current) return;
switch (command.type) {
case 'SEEK':
playerRef.current.seekTo(command.time!);
break;
case 'PLAY_TOPIC':
playerRef.current.seekTo(command.topic!.startTime);
if (command.autoPlay) {
playerRef.current.playVideo();
}
break;
case 'PLAY':
playerRef.current.playVideo();
break;
case 'PAUSE':
playerRef.current.pauseVideo();
break;
}
onCommandExecuted();
}, [command]);
return <div ref={playerRef} />;
}
Pattern 3: Refs for Performance-Critical State
export function HighPerformanceComponent() {
// Use state for UI updates
const [selectedTheme, setSelectedTheme] = useState<string | null>(null);
// Use refs for frequently changing values that don't need re-renders
const selectedThemeRef = useRef<string | null>(null);
const nextRequestIdRef = useRef(0);
const activeRequestIdRef = useRef<number | null>(null);
const pendingRequestsRef = useRef(new Map<string, number>());
const handleThemeChange = async (theme: string) => {
// Generate unique request ID
const requestId = nextRequestIdRef.current++;
// Cancel previous request for this theme
const existingRequestId = pendingRequestsRef.current.get(theme);
if (existingRequestId !== undefined && existingRequestId === activeRequestIdRef.current) {
return; // Request already in progress
}
// Store request ID
pendingRequestsRef.current.set(theme, requestId);
activeRequestIdRef.current = requestId;
selectedThemeRef.current = theme;
// Update UI
setSelectedTheme(theme);
// Fetch data
const data = await fetchThemeData(theme);
// Check if this request is still relevant
if (activeRequestIdRef.current === requestId) {
// Process data...
}
};
return <div>...</div>;
}
Pattern 4: Memoized Setters for Child Components
export function ParentWithManyChildren() {
const [playAllIndex, setPlayAllIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
// Memoize setters to prevent child re-renders
const memoizedSetPlayAllIndex = useCallback((value: number | ((prev: number) => number)) => {
setPlayAllIndex(value);
}, []);
const memoizedSetIsPlaying = useCallback((value: boolean) => {
setIsPlaying(value);
}, []);
return (
<>
{/* Child won't re-render when other state changes */}
<PlaybackControls
index={playAllIndex}
setIndex={memoizedSetPlayAllIndex}
isPlaying={isPlaying}
setIsPlaying={memoizedSetIsPlaying}
/>
</>
);
}
Pattern 5: Parallel State Updates
export function DataFetchingPage() {
const [data1, setData1] = useState(null);
const [data2, setData2] = useState(null);
const [data3, setData3] = useState(null);
useEffect(() => {
const fetchAll = async () => {
// Fetch in parallel
const [result1, result2, result3] = await Promise.allSettled([
fetch('/api/data1').then(r => r.json()),
fetch('/api/data2').then(r => r.json()),
fetch('/api/data3').then(r => r.json())
]);
// Batch state updates to trigger single re-render
React.startTransition(() => {
if (result1.status === 'fulfilled') setData1(result1.value);
if (result2.status === 'fulfilled') setData2(result2.value);
if (result3.status === 'fulfilled') setData3(result3.value);
});
};
fetchAll();
}, []);
return <div>...</div>;
}
Pattern 6: Custom Hooks for Complex Logic
// Custom hook for elapsed time
export function useElapsedTimer(startTime: number | null) {
const [elapsedTime, setElapsedTime] = useState(0);
useEffect(() => {
if (!startTime) {
setElapsedTime(0);
return;
}
const interval = setInterval(() => {
setElapsedTime(Date.now() - startTime);
}, 1000);
return () => clearInterval(interval);
}, [startTime]);
return elapsedTime;
}
// Usage
const generationStartTime = useState<number | null>(null);
const elapsedTime = useElapsedTimer(generationStartTime);
console.log(`Generating for ${Math.floor(elapsedTime / 1000)}s`);
Pattern 7: Theme-Based Dynamic Content
export function ThemeBasedContent() {
const [baseTopics, setBaseTopics] = useState<Topic[]>([]);
const [selectedTheme, setSelectedTheme] = useState<string | null>(null);
const [themeTopicsMap, setThemeTopicsMap] = useState<Record<string, Topic[]>>({});
const [usedTopicKeys, setUsedTopicKeys] = useState<Set<string>>(new Set());
// Display topics based on selected theme
const displayedTopics = selectedTheme
? (themeTopicsMap[selectedTheme] || [])
: baseTopics;
const handleThemeSelect = async (theme: string) => {
setSelectedTheme(theme);
// Check cache first
if (themeTopicsMap[theme]) {
return; // Already loaded
}
// Fetch theme-specific topics
const newTopics = await fetch('/api/topics', {
method: 'POST',
body: JSON.stringify({
theme,
excludeKeys: Array.from(usedTopicKeys)
})
}).then(r => r.json());
// Update cache and used keys
setThemeTopicsMap(prev => ({
...prev,
[theme]: newTopics
}));
setUsedTopicKeys(prev => {
const newSet = new Set(prev);
newTopics.forEach(t => newSet.add(t.key));
return newSet;
});
};
return (
<div>
<ThemeSelector onSelect={handleThemeSelect} />
<TopicsList topics={displayedTopics} />
</div>
);
}
Best Practices
- Use refs for non-UI state - Don't trigger re-renders unnecessarily
- Batch related state updates - Use startTransition or update together
- Memoize callbacks - Prevent child component re-renders
- Clean up on unmount - Always cleanup timers, subscriptions, AbortControllers
- Use state machines - Explicit states prevent invalid state combinations
- Separate concerns - Loading state, data state, UI state
- Cache when possible - Avoid re-fetching with Map/Set caches
Common Pitfalls
- Too many useState calls - Group related state into objects
- Not cleaning up - Memory leaks from timers/subscriptions
- Passing non-memoized callbacks - Causes unnecessary re-renders
- Using state for everything - Use refs for non-UI values
- Not batching updates - Multiple state updates = multiple renders
- Forgetting dependencies - useEffect/useCallback need correct deps
- Mutating state - Always create new objects/arrays
Performance Optimization
// ❌ Bad: Multiple re-renders
function Component() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const update = () => {
setA(1); // Re-render 1
setB(2); // Re-render 2
setC(3); // Re-render 3
};
}
// ✅ Good: Single re-render
function Component() {
const [state, setState] = useState({ a: 0, b: 0, c: 0 });
const update = () => {
setState({ a: 1, b: 2, c: 3 }); // Re-render 1
};
}
// ✅ Better: Use startTransition for non-urgent updates
function Component() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const update = () => {
startTransition(() => {
setA(1);
setB(2);
setC(3);
});
};
}
Testing
import { renderHook, act } from '@testing/library/react';
test('useElapsedTimer increments over time', () => {
jest.useFakeTimers();
const { result } = renderHook(() => useElapsedTimer(Date.now()));
expect(result.current).toBe(0);
act(() => {
jest.advanceTimersByTime(5000);
});
expect(result.current).toBeGreaterThanOrEqual(5000);
});
Next Steps
- Extract common patterns into custom hooks
- Add state persistence with localStorage
- Implement undo/redo with state history
- Add state debugging with DevTools
- Create state machines with XState if needed
- Profile render performance with React DevTools
Related Skills
- Resilient Async Operations - Manage async state safely
- Type-Safe Form Validation - Validate state updates
- Advanced Text Search - Complex search state management
Built from production state management in TLDW video analysis UI