Claude-skill-registry complex-state-management
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.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/complex-state-management" ~/.claude/skills/majiayu000-claude-skill-registry-complex-state-management && rm -rf "$T"
manifest:
skills/data/complex-state-management/SKILL.mdsource content
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