Skip to main content Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill react-hooks-compositionThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... More from this repository
Related occupations SOC
Based on SOC occupation classification
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 |
) {
sessionId = ();
{ data, error, isLoading } = useSWR< >(
sessionId &&
process. . &&
(inputValue)
?
:
);
mappedData = ( {
(!data) ;
data.
. (isUsState)
. (formatMapboxLocation);
}, [data]);
{
: mappedData,
error,
isLoading
};
}
isValidSearchQuery = (
: | |
): value is => {
value === && value. (). >= ;
};
isUsState = ( : ): {
suggestion. ?. ?. === ;
};
formatMapboxLocation = (
:
): ({
: suggestion. ,
: suggestion. ,
: ,
});
undefined
const
useSessionId
const
MapboxSuggestResponse
env
NEXT_PUBLIC_MAPBOX_API_KEY
isValidSearchQuery
`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
useMemo
() =>
if
return
undefined
return
suggestions
filter
map
return
data
const
value
string
null
undefined
string
return
typeof
'string'
trim
length
2
const
suggestion
MapboxSuggestion
boolean =>
return
context
country
country_code
'us'
const
suggestion
MapboxSuggestion
LocationSuggestion =>
id
mapbox_id
displayName
name
region
'US'
Why This Works
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
useMemo prevents recomputation on every render
Dependency array [data] only recomputes when data changes
Filter and map operations are pure and testable
Pure functions can be tested independently
No React context needed for helper functions
Type guards provide runtime validation
Common Triggers
"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
isDebouncing: Indicates user is still typing
isLoading: Indicates network request in progress
Combined state provides smooth UX feedback
API calls only fire after 300ms of inactivity
Reduces unnecessary requests while typing
SWR caches results for instant display
Loading indicator appears immediately on typing
Prevents result flashing between debounce intervals
Clear feedback during both input and fetch phases
Common Triggers
"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
useCallback ensures requestPreciseLocation reference is stable
No dependencies means function never changes
Internal state updates don't recreate function
Context hook throws if used outside provider
TypeScript enforces correct usage
Clear error messages for misuse
Context value is an object with multiple properties
Provider manages both props and internal state
Custom hook abstracts context consumption
Common Triggers
"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
Discriminated unions enable exhaustive checking
State-specific data is type-safe
Compiler catches missing state handlers
State configuration in one place
Easy to add new states
Clear separation of state and UI
Common Triggers
"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