Loading...
Loading...
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.
npx skill4agent add vishalsachdev/claude-skills complex-state-management'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>
);
}// 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} />;
}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>;
}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}
/>
</>
);
}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>;
}// 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`);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>
);
}// ❌ 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);
});
};
}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);
});