toolchains/javascript/frameworks/react/react-hooks-composition/SKILL.md
Advanced React hooks composition patterns - SWR integration, debounced search, memoized contexts, state machines, and performance optimization
npx skillsauth add bobmatnyc/claude-mpm-skills react-hooks-compositionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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:
Compose SWR with conditional logic, data transformation, and pure helper functions.
import useSWR from 'swr';
import { useMemo } from 'react';
// Type definitions
interface MapboxSuggestion {
name: string;
mapbox_id: string;
context?: {
country?: {
country_code: string;
};
};
}
interface MapboxSuggestResponse {
suggestions: MapboxSuggestion[];
}
interface LocationSuggestion {
id: string;
displayName: string;
region: string;
}
// Custom hook with conditional fetching
export function useMapboxLocationSuggestions(
inputValue: string | null | undefined
) {
const sessionId = useSessionId();
// Conditional SWR key - null disables fetching
const { data, error, isLoading } = useSWR<MapboxSuggestResponse>(
// Key is null (no fetch) unless all conditions met
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
);
// Transform data with useMemo for performance
const mappedData = useMemo(() => {
if (!data) return undefined;
return data.suggestions
.filter(isUsState)
.map(formatMapboxLocation);
}, [data]);
return {
data: mappedData,
error,
isLoading
};
}
// Pure helper functions (outside component/hook)
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',
});
Conditional Fetching:
nullisValidSearchQuery ensures type safetyData Transformation:
useMemo prevents recomputation on every render[data] only recomputes when data changesTestability:
Use this pattern when:
Combine debounced input with SWR fetching, tracking both debouncing and network loading states.
import { useState } from 'react';
import useSWR from 'swr';
// Debounce hook
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;
}
// Search component with dual loading states
export function SearchComponent() {
const [searchInput, setSearchInput] = useState('');
const debouncedSearchInput = useDebounce(searchInput, 300);
// Track if we're still debouncing (user is typing)
const isDebouncing = searchInput !== debouncedSearchInput;
// Fetch with debounced value
const { data = [], isLoading } = useLocationSuggestions(debouncedSearchInput);
// Combined loading state: debouncing OR fetching
const suggestionsLoading = isDebouncing || isLoading;
return (
<SearchInput
value={searchInput}
onChange={setSearchInput}
loading={suggestionsLoading}
suggestions={data}
placeholder="Search locations..."
/>
);
}
Dual Loading States:
isDebouncing: Indicates user is still typingisLoading: Indicates network request in progressPerformance:
User Experience:
Use this pattern when:
Create context providers with memoized values to prevent unnecessary re-renders of consumers.
import { createContext, useContext, useState, useCallback, useMemo, ReactNode } from 'react';
// Context value type
interface LocationContextValue {
location: UserLocation | null;
requestPreciseLocation: () => Promise<UserLocation | null>;
}
interface UserLocation {
latitude: number;
longitude: number;
accuracy: 'coarse' | 'precise';
}
// Create context with undefined default
const UserLocationContext = createContext<LocationContextValue | undefined>(
undefined
);
// Provider component
export const UserLocationProvider = ({
location: initialLocation = null,
children
}: {
location?: UserLocation | null;
children: ReactNode;
}) => {
const [preciseLocation, setPreciseLocation] = useState<UserLocation | null>(null);
// Stable callback reference with useCallback
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;
}
}, []); // No dependencies - function is stable
// Memoized context value prevents unnecessary re-renders
const contextValue = useMemo<LocationContextValue>(() => ({
location: preciseLocation || initialLocation,
requestPreciseLocation,
}), [initialLocation, preciseLocation, requestPreciseLocation]);
return (
<UserLocationContext.Provider value={contextValue}>
{children}
</UserLocationContext.Provider>
);
};
// Type-safe hook for consuming context
export const useUserLocation = (): LocationContextValue => {
const context = useContext(UserLocationContext);
if (context === undefined) {
throw new Error('useUserLocation must be used within UserLocationProvider');
}
return context;
};
// Helper function (can be in separate file)
async function getUserCoordinates(): Promise<GeolocationCoordinates | null> {
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
(position) => resolve(position.coords),
() => resolve(null),
{ enableHighAccuracy: true }
);
});
}
Memoization Prevents Re-renders:
useMemo for context value objectStable References:
useCallback ensures requestPreciseLocation reference is stableType Safety:
Best Practices:
Use this pattern when:
Use discriminated unions and state machines for predictable UI state management.
import { useState } from 'react';
// Discriminated union for request states
type RequestState = 'idle' | 'pending' | 'success' | 'error';
// State-specific configuration
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',
},
};
// Component using state machine
export function AsyncButton({
onClick
}: {
onClick: () => Promise<void>;
}) {
const [state, setState] = useState<RequestState>('idle');
const handleClick = async () => {
// State transition: idle/error → pending
setState('pending');
try {
await onClick();
// State transition: pending → success
setState('success');
// Auto-reset after 2 seconds
setTimeout(() => setState('idle'), 2000);
} catch (error) {
// State transition: pending → error
setState('error');
}
};
const config = stateConfig[state];
return (
<button
onClick={handleClick}
disabled={config.disabled}
className={`btn btn-${config.variant}`}
>
{config.label}
</button>
);
}
For more complex states with state-specific data:
// Tagged union with discriminated state
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 };
}
// Usage in component
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>
);
}
Predictable State Transitions:
Type Safety:
Maintainability:
Use this pattern when:
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);
// Determine current state
const isDebouncing = searchInput !== debouncedInput;
// Conditional SWR fetching
const { data, error, isLoading } = useSWR(
debouncedInput.length >= 2
? `/api/search?q=${encodeURIComponent(debouncedInput)}`
: null
);
// Compute search state
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]);
// State-specific UI
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>
);
}
// BAD: New object on every render causes unnecessary re-renders
function MyProvider({ children }) {
const [state, setState] = useState(null);
return (
<MyContext.Provider value={{ state, setState }}>
{children}
</MyContext.Provider>
);
}
// GOOD: Memoize the context value
function MyProvider({ children }) {
const [state, setState] = useState(null);
const value = useMemo(() => ({ state, setState }), [state]);
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
}
// BAD: Fetches even with empty input
function useSearch(query: string) {
const { data } = useSWR(`/api/search?q=${query}`);
return data;
}
// GOOD: Only fetch when query is valid
function useSearch(query: string) {
const { data } = useSWR(
query.trim().length >= 2 ? `/api/search?q=${query}` : null
);
return data;
}
// BAD: Only shows loading during network request
function Search() {
const [input, setInput] = useState('');
const debounced = useDebounce(input, 300);
const { data, isLoading } = useSWR(`/api?q=${debounced}`);
// Spinner disappears while debouncing!
return <>{isLoading && <Spinner />}</>;
}
// GOOD: Show loading during both debounce and fetch
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 />}</>;
}
// BAD: Easy to typo, no autocomplete
function Component() {
const [status, setStatus] = useState('idel'); // Typo!
if (status === 'idle') { // Won't match
return <div>Ready</div>;
}
}
// GOOD: Use discriminated union
type Status = 'idle' | 'loading' | 'success' | 'error';
function Component() {
const [status, setStatus] = useState<Status>('idle');
if (status === 'idle') { // Type-safe
return <div>Ready</div>;
}
}
null to prevent fetching when conditions aren't metuseMemo for expensive data transformationsuseCallback for functions in context or dependenciesSee 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.
tools
Xquik X data automation API - Use REST or MCP for tweet search, user lookup, follower exports, media downloads, monitors, webhooks, giveaway draws, and confirmation-gated X actions.
tools
LinkedIn automation via the Linked API CLI - fetch profiles, search people and companies, send messages, manage connections, create posts, react, comment, and run Sales Navigator and custom workflows. Use when the user wants to interact with LinkedIn.
tools
MCP (Model Context Protocol) server build and evaluation guide, including local conventions for tool surfaces, config, and testing
tools
MCP (Model Context Protocol) - Build AI-native servers with tools, resources, and prompts. TypeScript/Python SDKs for Claude Desktop integration.