| name | react-hooks-patterns |
| user-invocable | false |
| description | Use when React Hooks patterns including useState, useEffect, useContext, useMemo, useCallback, and custom hooks. Use for modern React development. |
| allowed-tools | ["Bash","Read"] |
React Hooks Patterns
Master React Hooks to build modern, functional React components.
This skill covers built-in hooks, custom hooks, and advanced patterns
for state management and side effects.
useState Hook
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(prev => prev - 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick=
{decrement}>-</button>
</div>
);
}
interface User {
name: string;
email: string;
}
function UserForm() {
const [user, setUser] = useState<User>({
name: '',
email: ''
});
const updateField = (field: keyof User, value: string) => {
setUser(prev => ({ ...prev, [field]: value }));
};
return (
<form>
<input
value={user.name}
onChange={(e) => updateField('name', e.target.value)}
/>
<input
value={user.email}
onChange={(e) => updateField('email', e.target.value)}
/>
</form>
);
}
useEffect Hook
import { useEffect, useState } from 'react';
function DataFetcher({ userId }: { userId: number }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const result = await response.json();
if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled) {
setError(err as Error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
{
cancelled = ;
};
}, [userId]);
(loading) ;
(error) ;
;
}
useContext Hook
import { createContext, useContext, useState, ReactNode } from 'react';
interface Theme {
mode: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<Theme | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [mode, setMode] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setMode(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ mode, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw ();
}
context;
}
() {
{ mode, toggleTheme } = ();
(
);
}
useMemo and useCallback
import { useMemo, useCallback, useState } from 'react';
function ExpensiveComponent({ items }: { items: number[] }) {
const [filter, setFilter] = useState('');
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.toString().includes(filter)
);
}, [items, filter]);
const handleFilterChange = useCallback((value: string) => {
setFilter(value);
}, []);
return (
<div>
<input
value={filter}
onChange={(e) => handleFilterChange(e.target.value)}
/>
<ItemList items={filteredItems} />
</div>
);
}
Custom Hooks
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = (value);
( {
handler = ( {
(value);
}, delay);
{
(handler);
};
}, [value, delay]);
debouncedValue;
}
() {
[searchTerm, setSearchTerm] = ();
debouncedSearchTerm = (searchTerm, );
( {
(debouncedSearchTerm) {
.(, debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
(
);
}
useReducer for Complex State
import { useReducer } from 'react';
interface State {
count: number;
history: number[];
}
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'RESET' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1,
history: [...state.history, state.count + 1]
};
case 'DECREMENT':
return {
count: state.count - 1,
history: [...state.history, state.count - 1]
};
case 'RESET':
return { count: 0, history: [0] };
:
state;
}
}
() {
[state, dispatch] = (reducer, {
: ,
: []
});
(
);
}
{
: {
: ;
: ;
: ;
};
: {
?: ;
?: ;
?: ;
};
: {
: ;
: ;
: ;
};
: ;
}
=
| { : ; : ; : | }
| { : ; : ; : }
| { : ; : }
| { : }
| { : }
| { : }
| { : };
(): {
(action.) {
:
{
...state,
: { ...state., [action.]: action. }
};
:
{
...state,
: { ...state., [action.]: action. }
};
:
{
...state,
: { ...state., [action.]: }
};
:
{ ...state, : };
:
{ ...state, : };
:
{ ...state, : };
:
{
: { : , : , : },
: {},
: { : , : , : },
:
};
:
state;
}
}
() {
[state, dispatch] = (formReducer, {
: { : , : , : },
: {},
: { : , : , : },
:
});
= () => {
e.();
({ : });
{
(state.);
({ : });
} (error) {
({ : });
}
};
(
);
}
useRef Hook
import { useRef, useEffect, useState } from 'react';
function FocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
function Timer() {
const intervalRef = useRef<number | null>(null);
const [count, setCount] = useState(0);
const start = () => {
if (intervalRef.current !== null) return;
intervalRef.current = window.setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.);
intervalRef. = ;
}
};
( {
{
(intervalRef. !== ) {
(intervalRef.);
}
};
}, []);
(
);
}
usePrevious<T>(: T): T | {
ref = useRef<T>();
( {
ref. = value;
}, [value]);
ref.;
}
() {
[count, setCount] = ();
prevCount = (count);
(
);
}
useLayoutEffect for DOM Measurements
import { useLayoutEffect, useRef, useState } from 'react';
function TooltipWithMeasurement() {
const [tooltipHeight, setTooltipHeight] = useState(0);
const tooltipRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (tooltipRef.current) {
const { height } = tooltipRef.current.getBoundingClientRect();
setTooltipHeight(height);
}
}, []);
return (
<div>
<div
ref={tooltipRef}
style={{
position: 'absolute',
top: `calc(100% + ${tooltipHeight}px)`
}}
>
Tooltip content
</div>
</div>
);
}
function SyncedScrollPanels() {
const leftRef = useRef<HTMLDivElement>(null);
const rightRef = useRef<HTMLDivElement>();
( {
left = leftRef.;
right = rightRef.;
(!left || !right) ;
= () => {
{
target. = source.;
};
};
leftHandler = (left, right);
rightHandler = (right, left);
left.(, leftHandler);
right.(, rightHandler);
{
left.(, leftHandler);
right.(, rightHandler);
};
}, []);
(
);
}
useImperativeHandle with forwardRef
import {
useRef,
useImperativeHandle,
forwardRef,
useState
} from 'react';
interface VideoPlayerHandle {
play: () => void;
pause: () => void;
seek: (time: number) => void;
}
interface VideoPlayerProps {
src: string;
}
const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
(props, ref) => {
const videoRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
useImperativeHandle(ref, () => ({
play: () => {
videoRef.current?.play();
setIsPlaying(true);
},
pause: () => {
videoRef.current?.pause();
setIsPlaying(false);
},
seek: () => {
(videoRef.) {
videoRef.. = time;
}
}
}), []);
(
);
}
);
() {
playerRef = useRef<>();
(
);
}
{
: ;
: ;
: ;
}
= forwardRef<, { ?: }>(
{
inputRef = useRef<>();
(ref, ({
: {
inputRef.?.();
},
: {
(inputRef.) {
inputRef.. = ;
}
},
: {
inputRef.?. || ;
}
}), []);
;
}
);
Custom Hooks Composition Patterns
import { useState, useEffect, useCallback } from 'react';
function useAsync<T>(asyncFunction: () => Promise<T>) {
const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
const [value, setValue] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const execute = useCallback(() => {
setStatus('pending');
setValue(null);
setError(null);
return asyncFunction()
.then((response) => {
setValue(response);
setStatus('success');
})
.catch((error) => {
setError(error);
setStatus('error');
});
}, [asyncFunction]);
return { execute, status, value, error };
}
function useFetch<T>(url: ) {
fetchData = (
(url).( res.() <T>),
[url]
);
{ execute, status, value, error } = useAsync<T>(fetchData);
( {
();
}, [execute]);
{ : value, : status === , error };
}
useForm<T <, >>(: T) {
[values, setValues] = useState<T>(initialValues);
[errors, setErrors] = useState<<<keyof T, >>>({});
[touched, setTouched] = useState<<<keyof T, >>>({});
[isSubmitting, setIsSubmitting] = ();
handleChange = ( {
( ({ ...prev, [field]: value }));
}, []);
handleBlur = ( {
( ({ ...prev, [field]: }));
}, []);
handleSubmit = (
(
: <>,
?: <<keyof T, >>
) => {
(validate) {
validationErrors = (values);
(validationErrors);
(.(validationErrors). > ) ;
}
();
{
(values);
} {
();
}
},
[values]
);
reset = ( {
(initialValues);
({});
({});
();
}, [initialValues]);
{
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset
};
}
() {
{
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset
} = ({
: ,
: ,
:
});
= () => {
: <<keyof values, >> = {};
(!vals.) errs. = ;
(!vals.) errs. = ;
errs;
};
(
);
}
Advanced useCallback and useMemo Optimization
import { useState, useCallback, useMemo, memo } from 'react';
interface Item {
id: number;
name: string;
category: string;
price: number;
}
interface Props {
items: Item[];
}
const ItemList = memo(({ items }: Props) => {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
function OptimizedShop() {
const [items] = useState<Item[]>([
{ id: 1, name: 'Apple', category: 'fruit', price: 1.5 },
{ id: 2, name: 'Banana', category: 'fruit', price: },
{ : , : , : , : }
]);
[searchTerm, setSearchTerm] = ();
[selectedCategory, setSelectedCategory] = useState<>();
[sortBy, setSortBy] = useState< | >();
filteredItems = ( {
items.( {
matchesSearch = item.
.()
.(searchTerm.());
matchesCategory =
selectedCategory === || item. === selectedCategory;
matchesSearch && matchesCategory;
});
}, [items, searchTerm, selectedCategory]);
sortedItems = ( {
[...filteredItems].( {
(sortBy === ) {
a..(b.);
}
a. - b.;
});
}, [filteredItems, sortBy]);
categories = ( {
uniqueCategories = (items.( item.));
[, ....(uniqueCategories)];
}, [items]);
handleSearch = ( {
(value);
}, []);
handleCategoryChange = ( {
(category);
}, []);
handleSortChange = ( {
(sort);
}, []);
(
);
}
useEventCallback<T (...: []) => >(: T): T {
ref = useRef<T>(fn);
( {
ref. = fn;
});
(
( ref.(...args)) T,
[]
);
}
() {
[count, setCount] = ();
handleSubmit = ( {
.(, count);
});
(
);
}
Advanced Hook Patterns
import { useState, useEffect, useCallback, useRef } from 'react';
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
function Clock() {
const [time, setTime] = useState(new Date());
useInterval(() => {
setTime(new Date());
}, 1000);
return <div>{time.toLocaleTimeString()}</div>;
}
function useOnScreen() {
[isIntersecting, setIntersecting] = ();
( {
(!ref.) ;
observer = (
(entry.)
);
observer.(ref.);
{
observer.();
};
}, [ref]);
isIntersecting;
}
() {
ref = useRef<>();
isVisible = (ref);
(
);
}
(): {
[matches, setMatches] = ();
( {
media = .(query);
(media. !== matches) {
(media.);
}
= () => (media.);
media.(, listener);
media.(, listener);
}, [matches, query]);
matches;
}
() {
isMobile = ();
isTablet = ();
isDesktop = ();
(
);
}
() {
( {
= () => {
(!ref. || ref..(event. )) {
;
}
(event);
};
.(, listener);
.(, listener);
{
.(, listener);
.(, listener);
};
}, [ref, handler]);
}
() {
[isOpen, setIsOpen] = ();
ref = useRef<>();
(ref, ());
(
);
}
(): [, ] {
[value, setValue] = (initialValue);
toggle = ( ( !v), []);
[value, toggle];
}
() {
[isOn, toggle] = ();
(
);
}
useArray<T>(: T[]) {
[array, setArray] = (initialValue);
push = ( {
( [...a, element]);
}, []);
filter = ( {
( a.(callback));
}, []);
update = ( {
( [
...a.(, index),
newElement,
...a.(index + )
]);
}, []);
remove = ( {
( [...a.(, index), ...a.(index + )]);
}, []);
clear = ( {
([]);
}, []);
{ array, : setArray, push, filter, update, remove, clear };
}
() {
{ : todos, push, remove, update } = useArray<{
: ;
: ;
: ;
}>([]);
= () => {
({ : .(), text, : });
};
= () => {
todo = todos[index];
(index, { ...todo, : !todo. });
};
(
);
}
When to Use This Skill
Use react-hooks-patterns when you need to:
- Build modern React applications with functional components
- Manage component state with useState and useReducer
- Handle side effects with useEffect
- Share state across components with useContext
- Optimize performance with useMemo and useCallback
- Create reusable logic with custom hooks
- Access DOM elements with useRef
- Build maintainable React applications
- Follow React best practices and patterns
Best Practices
- Use functional updates when new state depends on previous state
- Always clean up side effects in useEffect return function
- Include all dependencies in useEffect dependency array
- Use useCallback to memoize functions passed to child components
- Use useMemo only for expensive computations, not simple values
- Create custom hooks to encapsulate and reuse stateful logic
- Use useReducer for complex state logic with multiple sub-values
- Keep hooks at the top level of components, never in conditions
- Name custom hooks with "use" prefix for linting and conventions
- Use TypeScript for type safety and better developer experience
- Separate concerns by creating focused custom hooks
- Use useRef for values that don't trigger re-renders
- Prefer useLayoutEffect only when measuring DOM or preventing flicker
- Use memo() with components that receive callback props
- Compose hooks to build more complex behaviors from simple ones
- Use useImperativeHandle sparingly, prefer declarative patterns
- Avoid premature optimization with useMemo and useCallback
- Keep dependency arrays honest, use ESLint exhaustive-deps rule
- Extract complex logic into custom hooks for testability
- Use useContext for global state, not prop drilling
Common Pitfalls
- Forgetting to include dependencies in useEffect array
- Not cleaning up side effects leading to memory leaks
- Overusing useCallback and useMemo causing premature optimization
- Calling hooks conditionally or inside loops (violates Rules of Hooks)
- Not handling async operations properly in useEffect
- Creating infinite loops by updating state in useEffect incorrectly
- Mutating ref.current during render instead of in effects
- Using stale closures in callbacks without proper dependencies
- Not using functional updates with useState when needed
- Setting state on unmounted components
- Using object or array literals in dependency arrays
- Not memoizing expensive calculations that run on every render
- Confusing useEffect with useLayoutEffect use cases
- Creating unnecessary re-renders by not memoizing callbacks
- Using useState for values that should be refs
- Not using cleanup functions for event listeners and subscriptions
- Forgetting that useEffect runs after paint, not before
- Creating tightly coupled custom hooks that are hard to reuse
- Over-abstracting with custom hooks too early
- Ignoring ESLint warnings about dependency arrays
Resources
Official React Documentation
Guides and Best Practices
TypeScript Resources
Additional Resources