React patterns & Hooks
Guide Agents to use custom Hook extraction, state lifting, and data-fetch patterns while honoring eslint-plugin-react-hooks and project folder conventions.
The SKILL should separate server vs client components (if using App Router), legitimate useEffect uses vs alternatives (events, key, derived state), and when useMemo/useCallback are warranted instead of default wrapping.
For data fetching, state rules for TanStack Query, SWR, or RSC fetch; loading and error behavior under Suspense boundaries must be predictable.
Context needs documented granularity and split strategies to avoid huge global objects and useless re-renders; list key and a11y attributes in the same pattern notes.
- Strict Mode and concurrent features: if enabled, document expected double effect invocation and testing style.
- Forms: controlled vs uncontrolled and integration with zod or similar validators.
- Testing: @testing-library query priority and assertions that avoid implementation details.
Custom Hooks and Context+Reducer State Management
// Custom Hook design: useAsync — encapsulate async request state
function useAsync<T>(asyncFn: () => Promise<T>, deps: React.DependencyList) {
const [state, dispatch] = React.useReducer(asyncReducer<T>, {
status: 'idle', data: null, error: null
});
React.useEffect(() => {
let cancelled = false; // prevent race conditions
dispatch({ type: 'LOADING' });
asyncFn().then(
data => { if (!cancelled) dispatch({ type: 'SUCCESS', data }); },
error => { if (!cancelled) dispatch({ type: 'ERROR', error }); }
);
return () => { cancelled = true; }; // cleanup: cancel on unmount or dep change
}, deps); // eslint: exhaustive-deps
return state;
}
// useDebounce: debounce a value to avoid rapid API calls
function useDebounce<T>(value: T, delayMs: number): T {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(timer); // clear previous timer
}, [value, delayMs]);
return debounced;
}
// Context + Reducer: lightweight global state without Redux
type CartAction =
| { type: 'ADD_ITEM'; item: CartItem }
| { type: 'REMOVE_ITEM'; id: string }
| { type: 'CLEAR' };
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.item] };
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter(i => i.id !== action.id) };
case 'CLEAR':
return { items: [] };
}
}
// Split state and dispatch into separate Contexts to avoid unnecessary re-renders
const CartStateCtx = React.createContext<CartState | null>(null);
const CartDispatchCtx = React.createContext<React.Dispatch<CartAction> | null>(null);
export function CartProvider({ children }) {
const [state, dispatch] = React.useReducer(cartReducer, { items: [] });
return (
<CartStateCtx.Provider value={state}>
<CartDispatchCtx.Provider value={dispatch}>
{children}
</CartDispatchCtx.Provider>
</CartStateCtx.Provider>
);
}
- Single responsibility: one primary exported component per file by default; split complex UI into children or colocated private modules—avoid thousand-line JSX files.
- Compound components: parent shares implicit state via Context or explicit child APIs (e.g.
Tabs.Tab) with a stable external contract. - Controlled wrappers: unify value/onChange shape at library boundaries; avoid half-controlled mixes that tear state.
Composition and slots
Prefer children, render props, or explicit slot props over long conditional trees of child type strings in the parent. The SKILL can require “extension points” to be exposed only through composition for typing and test doubles.
┌──────────────────┐
│ Parent │
│ state / data │
└────────┬─────────┘
│ props.children / render prop / named slots
┌────────┴────────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ Region A │ │ Region B │
│ (display) │ │ (list/form)│
└────────────┘ └────────────┘
│
Optional: shared implicit state via small Context
Render props suit “same logic, different UI”; when children is a function, type the parameters explicitly to avoid implicit any in generated code.
Performance Optimization, Error Boundaries, and Suspense
// useMemo / useCallback: avoid expensive recalculations and unstable references
function ProductList({ products, filter }: Props) {
// ✅ filteredProducts recalculated only when products or filter changes
const filtered = React.useMemo(
() => products.filter(p => p.category === filter),
[products, filter]
);
// ✅ handleClick is stable — won't cause unnecessary child re-renders
const handleClick = React.useCallback((id: string) => {
router.push(`/product/${id}`);
}, []); // depends on no component state, so deps array is empty
return <List items={filtered} onItemClick={handleClick} />;
}
// Error Boundary: catch render errors in the component tree
class ErrorBoundary extends React.Component<
{ fallback: React.ReactNode; children: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
logger.error(error, info.componentStack); // report to monitoring service
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
// Suspense + React.lazy: code-split heavy components for faster initial load
const DataChart = React.lazy(() => import('./DataChart'));
function Dashboard() {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<React.Suspense fallback={<Skeleton />}>
<DataChart />
</React.Suspense>
</ErrorBoundary>
);
}
- Dependency arrays: use exhaustive-deps for
useEffect/useMemo/useCallback; missing deps cause stale closures, while inline objects/arrays in deps cause extra runs—stabilize references first. - Effect purpose: synchronize with external systems (subscriptions, DOM, non-React stores); if an event handler, derived value, or
keyreset solves it, do not use an effect. - Cleanup: unsubscribe and clear timers in cleanup; Strict Mode in dev may double-mount effects to surface missing cleanup.
- Performance hooks: do not wrap everything in
useMemo/useCallbackby default; add when childmemodepends on referential equality or when values are deps of other Hooks, and comment why.
setState(s => …) or useRef for the latest handle (pick per scenario).
In-page tools: dependency explainer and Hooks quiz
The script below runs only on this page to clarify useEffect dependency semantics and reinforce Hook rules (not a linter replacement).
useEffect dependency array explainer
Enter comma-separated dependency names (same as your source array), e.g. userId, query, options. Enter [] alone for an empty array.
Hooks rules mini-quiz (4 questions)
Pick one answer per question; submit to see score and short hints.
SKILL outline
---
name: react-patterns-hooks
description: Implement React features with Hook rules and data-fetch patterns
---
# Rules
1. Follow the Rules of Hooks: call Hooks only at the top level and only in function components or custom Hooks
2. Always satisfy exhaustive-deps: all reactive variables in useEffect/useMemo/useCallback deps arrays must be listed
3. Prevent stale closures: use functional setState(s => …) or a useRef for the latest value in event callbacks
4. Add useMemo/useCallback only when child memo depends on stable reference equality—not everywhere by default
5. Split read and dispatch into separate Contexts to prevent unnecessary re-renders from dispatch updates
# Steps
1. Identify reusable async state logic → extract useAsync Custom Hook; include cancellation and race-condition guard
2. Wrap timer-based debounce/throttle logic in a Custom Hook (useDebounce/useThrottle) and return a stable ref
3. For multi-field or complex state, use useReducer with a pure reducer instead of multiple useState calls
4. Provide state context and dispatch context separately; export typed consumer Hooks (useCartState / useCartDispatch)
5. Wrap expensive derived calculations in useMemo; wrap event callback props passed to React.memo children in useCallback
6. Place an ErrorBoundary at each data-fetching boundary; implement componentDidCatch to report errors to the monitoring service
7. Code-split heavy components (charts, editors) with React.lazy + Suspense; provide a skeleton fallback UI
8. Effects must return a cleanup function; when the async effect may be stale, use a cancelled flag or AbortController
9. In App Router: default to Server Components; mark as 'use client' only when hooks/browser APIs are required
10. Test Hooks with renderHook from @testing-library/react; test async behavior with act and waitFor