| 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() {
const [pageState, setPageState] = useState<PageState>('IDLE');
const [loadingStage, setLoadingStage] = useState<LoadingStage>(null);
const [error, setError] = useState<string>('');
const [generationStartTime, setGenerationStartTime] = useState<number | null>(null);
const [processingStartTime, setProcessingStartTime] = useState<number | null>(null);
const abortManager = useRef(new AbortManager());
const handleAnalyze = async () => {
try {
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());
setLoadingStage('understanding');
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());
setLoadingStage('processing');
setProcessingStartTime(Date.now());
setPageState('IDLE');
setLoadingStage(null);
setGenerationStartTime(null);
} catch (error) {
setPageState('ERROR');
setError(error.message);
}
};
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
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;
}
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 (
);
}
() {
playerRef = useRef<>();
( {
(!command || !playerRef.) ;
(command.) {
:
playerRef..(command.!);
;
:
playerRef..(command.!.);
(command.) {
playerRef..();
}
;
:
playerRef..();
;
:
playerRef..();
;
}
();
}, [command]);
;
}
Pattern 3: Refs for Performance-Critical State
export function HighPerformanceComponent() {
const [selectedTheme, setSelectedTheme] = useState<string | null>(null);
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) => {
const requestId = nextRequestIdRef.current++;
const existingRequestId = pendingRequestsRef.current.get(theme);
if (existingRequestId !== undefined && existingRequestId === activeRequestIdRef.current) {
return;
}
pendingRequestsRef.current.(theme, requestId);
activeRequestIdRef. = requestId;
selectedThemeRef. = theme;
(theme);
data = (theme);
(activeRequestIdRef. === requestId) {
}
};
;
}
Pattern 4: Memoized Setters for Child Components
export function ParentWithManyChildren() {
const [playAllIndex, setPlayAllIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
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 () => {
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())
]);
React.startTransition(() => {
if (result1.status === 'fulfilled') setData1(result1.);
(result2. === ) (result2.);
(result3. === ) (result3.);
});
};
();
}, []);
;
}
Pattern 6: Custom Hooks for Complex Logic
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;
}
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());
const displayedTopics = selectedTheme
? (themeTopicsMap[selectedTheme] || [])
: baseTopics;
const handleThemeSelect = async (theme: string) => {
setSelectedTheme(theme);
if (themeTopicsMap[theme]) {
return;
}
const newTopics = await fetch('/api/topics', {
method: 'POST',
body: JSON.stringify({
theme,
excludeKeys: Array.from(usedTopicKeys)
})
}).then( r.());
( ({
...prev,
[theme]: newTopics
}));
( {
newSet = (prev);
newTopics.( newSet.(t.));
newSet;
});
};
(
);
}
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
function Component() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const update = () => {
setA(1);
setB(2);
setC(3);
};
}
function Component() {
const [state, setState] = useState({ a: 0, b: 0, c: 0 });
const update = () => {
setState({ a: 1, b: 2, c: 3 });
};
}
function Component() {
const [a, setA] = ();
[b, setB] = ();
[c, setC] = ();
= () => {
( {
();
();
();
});
};
}
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