| name | react-hooks-composition |
| description | Advanced React hooks composition patterns - SWR integration, debounced search, memoized contexts, state machines, and performance optimization |
| user-invocable | false |
| disable-model-invocation | true |
| version | 1.0.1 |
| category | toolchain |
| author | Claude MPM Team |
| license | MIT |
| progressive_disclosure | {"entry_point":{"summary":"Master React hooks composition: custom hooks with SWR, debounced state, memoized contexts, type-safe patterns","when_to_use":"Building data-fetching hooks, search interfaces, context providers, async UI states, performance-critical components","quick_start":"1. Compose SWR with conditional keys 2. Add debounce with dual loading states 3. Memoize context values 4. Use state machines for UI states"}} |
| context_limit | 700 |
| tags | ["react","hooks","composition","swr","performance","typescript","custom-hooks","useMemo","useCallback","context","debounce","state-machine"] |
| requires_tools | [] |
React Hooks Composition Patterns
Overview
Advanced patterns for composing React hooks to create maintainable, performant, and type-safe custom hooks. Covers SWR integration, debounced search, memoized contexts, and state machine patterns.
Key Concepts:
- Conditional SWR fetching with null keys
- Debounced state with dual loading indicators
- Memoized context providers to prevent re-renders
- State machine pattern for predictable UI states
- Pure helper functions for testability
Pattern 1: SWR Hook Composition with Conditional Fetching
The Pattern
Compose SWR with conditional logic, data transformation, and pure helper functions.
import useSWR from 'swr';
import { useMemo } from 'react';
interface MapboxSuggestion {
name: string;
mapbox_id: string;
context?: {
country?: {
country_code: string;
};
};
}
interface MapboxSuggestResponse {
suggestions: MapboxSuggestion[];
}
interface LocationSuggestion {
id: string;
displayName: string;
region: string;
}
export function useMapboxLocationSuggestions(
inputValue: string | null | undefined
) {
const sessionId = useSessionId();
const { data, error, isLoading } = useSWR<MapboxSuggestResponse>(
sessionId &&
process.env.NEXT_PUBLIC_MAPBOX_API_KEY &&
isValidSearchQuery(inputValue)
? `https://api.mapbox.com/search/searchbox/v1/suggest?q=${encodeURIComponent(inputValue!)}&session_token=${sessionId}&access_token=${process.env.NEXT_PUBLIC_MAPBOX_API_KEY}`
: null
);
const mappedData = useMemo(() => {
if (!data) return undefined;
return data.suggestions
.filter(isUsState)
.map(formatMapboxLocation);
}, [data]);
return {
data: mappedData,
error,
isLoading
};
}
const isValidSearchQuery = (
value: string | null | undefined
): value is string => {
return typeof value === 'string' && value.trim().length >= 2;
};
const isUsState = (suggestion: MapboxSuggestion): boolean => {
return suggestion.context?.country?.country_code === 'us';
};
const formatMapboxLocation = (
suggestion: MapboxSuggestion
): LocationSuggestion => ({
id: suggestion.mapbox_id,
displayName: suggestion.name,
region: 'US',
});
Why This Works
Conditional Fetching:
- SWR doesn't fetch when key is
null
- All conditions checked before constructing URL
- Type guard
isValidSearchQuery ensures type safety
- Prevents unnecessary API calls on empty input
Data Transformation:
useMemo prevents recomputation on every render
- Dependency array
[data] only recomputes when data changes
- Filter and map operations are pure and testable
Testability:
- Pure functions can be tested independently
- No React context needed for helper functions
- Type guards provide runtime validation
Common Triggers
Use this pattern when:
- "fetch data based on user input"
- "conditional API calls with SWR"
- "transform API response data"
- "prevent fetching on empty search"
- "type-safe data fetching hooks"
Pattern 2: Debounced Search with Dual Loading States
The Pattern
Combine debounced input with SWR fetching, tracking both debouncing and network loading states.
import { useState } from 'react';
import useSWR from 'swr';
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
export function SearchComponent() {
const [searchInput, setSearchInput] = useState('');
const debouncedSearchInput = useDebounce(searchInput, 300);
const isDebouncing = searchInput !== debouncedSearchInput;
const { data = [], isLoading } = useLocationSuggestions(debouncedSearchInput);
const suggestionsLoading = isDebouncing || isLoading;
return (
<SearchInput
value={searchInput}
onChange={setSearchInput}
loading={suggestionsLoading}
suggestions={data}
placeholder="Search locations..."
/>
);
}
Why This Works
Dual Loading States:
isDebouncing: Indicates user is still typing
isLoading: Indicates network request in progress
- Combined state provides smooth UX feedback
Performance:
- API calls only fire after 300ms of inactivity
- Reduces unnecessary requests while typing
- SWR caches results for instant display
User Experience:
- Loading indicator appears immediately on typing
- Prevents result flashing between debounce intervals
- Clear feedback during both input and fetch phases
Common Triggers
Use this pattern when:
- "debounced search with loading state"
- "prevent too many API calls while typing"
- "show loading indicator during debounce"
- "smooth search experience"
- "combine debounce with data fetching"
Pattern 3: Memoized Context Provider
The Pattern
Create context providers with memoized values to prevent unnecessary re-renders of consumers.
import { createContext, useContext, useState, useCallback, useMemo, ReactNode } from 'react';
interface LocationContextValue {
location: UserLocation | null;
requestPreciseLocation: () => Promise<UserLocation | null>;
}
interface UserLocation {
latitude: number;
longitude: number;
accuracy: 'coarse' | 'precise';
}
const UserLocationContext = createContext<LocationContextValue | undefined>(
undefined
);
export const UserLocationProvider = ({
location: initialLocation = null,
children
}: {
location?: UserLocation | null;
children: ReactNode;
}) => {
const [preciseLocation, setPreciseLocation] = useState<UserLocation | null>(null);
const requestPreciseLocation = useCallback(async () => {
try {
const coords = await getUserCoordinates();
const location = coords
? {
latitude: coords.latitude,
longitude: coords.longitude,
accuracy: 'precise' as const
}
: null;
setPreciseLocation(location);
return location;
} catch (error) {
console.error('Failed to get precise location:', error);
return null;
}
}, []);
const contextValue = useMemo<LocationContextValue>(() => ({
location: preciseLocation || initialLocation,
requestPreciseLocation,
}), [initialLocation, preciseLocation, requestPreciseLocation]);
return (
<UserLocationContext.Provider value={contextValue}>
{children}
</UserLocationContext.Provider>
);
};
export const useUserLocation = (): LocationContextValue => {
const context = useContext(UserLocationContext);
if (context === undefined) {
throw new Error('useUserLocation must be used within UserLocationProvider');
}
return context;
};
async function getUserCoordinates(): Promise<GeolocationCoordinates | null> {
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
(position) => resolve(position.coords),
() => resolve(null),
{ enableHighAccuracy: true }
);
});
}
Why This Works
Memoization Prevents Re-renders:
useMemo for context value object
- Only recreates when dependencies change
- Consumers only re-render when values actually change
Stable References:
useCallback ensures requestPreciseLocation reference is stable
- No dependencies means function never changes
- Internal state updates don't recreate function
Type Safety:
- Context hook throws if used outside provider
- TypeScript enforces correct usage
- Clear error messages for misuse
Best Practices:
- Context value is an object with multiple properties
- Provider manages both props and internal state
- Custom hook abstracts context consumption
Common Triggers
Use this pattern when:
- "context causing too many re-renders"
- "optimize context provider performance"
- "stable callback in context"
- "memoized context value"
- "prevent unnecessary renders from context"
Pattern 4: State Machine for UI States
The Pattern
Use discriminated unions and state machines for predictable UI state management.
import { useState } from 'react';
type RequestState = 'idle' | 'pending' | 'success' | 'error';
const stateConfig: Record<RequestState, {
label: string;
disabled: boolean;
variant: 'primary' | 'success' | 'danger';
}> = {
idle: {
label: 'Click to start',
disabled: false,
variant: 'primary',
},
pending: {
label: 'Loading...',
disabled: true,
variant: 'primary',
},
success: {
label: 'Complete!',
disabled: false,
variant: 'success',
},
error: {
label: 'Failed - try again',
disabled: false,
variant: 'danger',
},
};
export function AsyncButton({
onClick
}: {
onClick: () => Promise<void>;
}) {
const [state, setState] = useState<RequestState>('idle');
const handleClick = async () => {
setState('pending');
try {
await onClick();
setState('success');
setTimeout(() => setState('idle'), 2000);
} catch (error) {
setState('error');
}
};
const config = stateConfig[state];
return (
<button
onClick={handleClick}
disabled={config.disabled}
className={`btn btn-${config.variant}`}
>
{config.label}
</button>
);
}
Advanced: Tagged Union with Data
For more complex states with state-specific data:
type AsyncState<T, E = Error> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: E };
function useAsyncOperation<T>(
operation: () => Promise<T>
): {
state: AsyncState<T>;
execute: () => Promise<void>;
reset: () => void;
} {
const [state, setState] = useState<AsyncState<T>>({ status: 'idle' });
const execute = async () => {
setState({ status: 'loading' });
try {
const data = await operation();
setState({ status: 'success', data });
} catch (error) {
setState({
status: 'error',
error: error instanceof Error ? error : new Error('Unknown error')
});
}
};
const reset = () => {
setState({ status: 'idle' });
};
return { state, execute, reset };
}
export function DataFetchButton() {
const { state, execute, reset } = useAsyncOperation(async () => {
const response = await fetch('/api/data');
return response.json();
});
return (
<div>
<button onClick={execute} disabled={state.status === 'loading'}>
Fetch Data
</button>
{state.status === 'loading' && <Spinner />}
{state.status === 'success' && (
<div>
<pre>{JSON.stringify(state.data, null, 2)}</pre>
<button onClick={reset}>Reset</button>
</div>
)}
{state.status === 'error' && (
<div className="error">
{state.error.message}
<button onClick={reset}>Try Again</button>
</div>
)}
</div>
);
}
Why This Works
Predictable State Transitions:
- Explicit states prevent impossible states
- Clear state transition logic
- TypeScript ensures all states handled
Type Safety:
- Discriminated unions enable exhaustive checking
- State-specific data is type-safe
- Compiler catches missing state handlers
Maintainability:
- State configuration in one place
- Easy to add new states
- Clear separation of state and UI
Common Triggers
Use this pattern when:
- "manage async operation states"
- "predictable UI state machine"
- "loading, success, error states"
- "prevent impossible states"
- "type-safe state management"
Advanced Composition: Combining Patterns
Complete Search Component
Combining debounced search, SWR fetching, and state machine:
import { useState, useMemo } from 'react';
import useSWR from 'swr';
type SearchState = 'idle' | 'debouncing' | 'fetching' | 'success' | 'error';
export function AdvancedSearch() {
const [searchInput, setSearchInput] = useState('');
const debouncedInput = useDebounce(searchInput, 300);
const isDebouncing = searchInput !== debouncedInput;
const { data, error, isLoading } = useSWR(
debouncedInput.length >= 2
? `/api/search?q=${encodeURIComponent(debouncedInput)}`
: null
);
const searchState = useMemo<SearchState>(() => {
if (error) return 'error';
if (isDebouncing) return 'debouncing';
if (isLoading) return 'fetching';
if (data) return 'success';
return 'idle';
}, [isDebouncing, isLoading, data, error]);
const showSpinner = searchState === 'debouncing' || searchState === 'fetching';
const showResults = searchState === 'success';
const showError = searchState === 'error';
return (
<div>
<input
type="text"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
placeholder="Search..."
/>
{showSpinner && <Spinner />}
{showResults && (
<SearchResults results={data.results} />
)}
{showError && (
<ErrorMessage>Failed to fetch results</ErrorMessage>
)}
</div>
);
}
Anti-Patterns to Avoid
❌ Don't: Create New Objects in Render
function MyProvider({ children }) {
const [state, setState] = useState(null);
return (
<MyContext.Provider value={{ state, setState }}>
{children}
</MyContext.Provider>
);
}
function MyProvider({ children }) {
const [state, setState] = useState(null);
const value = useMemo(() => ({ state, setState }), [state]);
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
}
❌ Don't: Forget SWR Conditional Fetching
function useSearch(query: string) {
const { data } = useSWR(`/api/search?q=${query}`);
return data;
}
function useSearch(query: string) {
const { data } = useSWR(
query.trim().length >= 2 ? `/api/search?q=${query}` : null
);
return data;
}
❌ Don't: Ignore Debounce vs Loading State
function Search() {
const [input, setInput] = useState('');
const debounced = useDebounce(input, 300);
const { data, isLoading } = useSWR(`/api?q=${debounced}`);
return <>{isLoading && <Spinner />}</>;
}
function Search() {
const [input, setInput] = useState('');
const debounced = useDebounce(input, 300);
const { data, isLoading } = useSWR(`/api?q=${debounced}`);
const isDebouncing = input !== debounced;
const loading = isDebouncing || isLoading;
return <>{loading && <Spinner />}</>;
}
❌ Don't: Use String States Without Type Safety
function Component() {
const [status, setStatus] = useState('idel');
if (status === 'idle') {
return <div>Ready</div>;
}
}
type Status = 'idle' | 'loading' | 'success' | 'error';
function Component() {
const [status, setStatus] = useState<Status>('idle');
if (status === 'idle') {
return <div>Ready</div>;
}
}
Best Practices Summary
- Conditional SWR Keys: Use
null to prevent fetching when conditions aren't met
- Memoize Transformations: Use
useMemo for expensive data transformations
- Stable Callbacks: Use
useCallback for functions in context or dependencies
- Memoize Context Values: Prevent unnecessary re-renders of context consumers
- Dual Loading States: Track both debouncing and network loading separately
- Pure Helper Functions: Extract logic outside components for testability
- Type-Safe States: Use discriminated unions for state machines
- Explicit State Transitions: Make state changes predictable and clear
Related Patterns
See the react-state-machine skill for more advanced state machine patterns with XState. See the react-advanced skill for React 19 platform features and rendering architecture — the React Compiler (which changes when manual memoization is needed), concurrent rendering, Actions, and context-optimization at scale.
References