| name | react-hooks-patterns |
| description | Master React Hooks including useState, useEffect, useContext, useReducer, and custom hooks with production-grade patterns |
| sasmp_version | 2.0.0 |
| bonded_agent | 02-hooks-patterns |
| bond_type | PRIMARY_BOND |
| input_validation | [object Object] |
| output_format | [object Object] |
| error_handling | [object Object] |
| observability | [object Object] |
React Hooks Patterns Skill
Overview
Master React Hooks patterns including built-in hooks (useState, useEffect, useContext, useReducer, useCallback, useMemo) and custom hook development for reusable logic.
Learning Objectives
- Understand all built-in React Hooks
- Create custom hooks for code reuse
- Implement advanced hook patterns
- Optimize performance with hooks
- Handle side effects effectively
Core Hooks
useState
const [state, setState] = useState(initialValue);
// Lazy initialization for expensive computations
const [state, setState] = useState(() => expensiveComputation());
// Functional updates when state depends on previous value
setState(prev => prev + 1);
useEffect
useEffect(() => {
// Side effect code
const subscription = api.subscribe();
// Cleanup function
return () => {
subscription.unsubscribe();
};
}, [dependencies]); // Dependency array
useContext
const value = useContext(MyContext);
// Best practice: Create custom hook
function useMyContext() {
const context = useContext(MyContext);
if (!context) {
throw new Error('useMyContext must be within Provider');
}
return context;
}
useReducer
const [state, dispatch] = useReducer(reducer, initialState);
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b], // Dependencies
);
useMemo
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b),
[a, b]
);
useRef
const ref = useRef(initialValue);
// DOM access
const inputRef = useRef(null);
<input ref={inputRef} />
inputRef.current.focus();
// Mutable value that doesn't trigger re-render
const countRef = useRef(0);
Custom Hooks Library
useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(url)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
useLocalStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value) => {
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];
}
useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
usePrevious
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
useToggle
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle];
}
useOnClickOutside
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
Practice Exercises
- Counter with useReducer: Build a counter using useReducer instead of useState
- Theme Context: Create a theme switcher using useContext
- Data Fetching: Implement useFetch with caching
- Form Handler: Create useForm custom hook for form state management
- Window Size Hook: Build useWindowSize hook for responsive layouts
- Intersection Observer: Create useInView hook for lazy loading
- Timer Hook: Build useInterval and useTimeout hooks
Common Patterns
Fetching with Abort
function useFetchWithAbort(url) {
const [state, setState] = useState({ data: null, loading: true, error: null });
useEffect(() => {
const abortController = new AbortController();
fetch(url, { signal: abortController.signal })
.then(res => res.json())
.then(data => setState({ data, loading: false, error: null }))
.catch(err => {
if (err.name !== 'AbortError') {
setState({ data: null, loading: false, error: err });
}
});
return () => abortController.abort();
}, [url]);
return state;
}
Combining Multiple Hooks
function useAuthenticatedApi(url) {
const { token } = useAuth();
const { data, loading, error } = useFetch(url, {
headers: {
Authorization: `Bearer ${token}`
}
});
return { data, loading, error };
}
Best Practices
- Name custom hooks with "use" prefix
- Extract reusable logic into custom hooks
- Keep hooks at component top level (no conditionals)
- Use ESLint plugin for hooks rules
- Document custom hook dependencies
- Test custom hooks with @testing-library/react-hooks
Resources
- React Hooks API Reference
- useHooks.com - Collection of custom hooks
- React Hooks Cheatsheet
Assessment
Can you:
- Explain when to use each built-in hook?
- Create custom hooks for reusable logic?
- Handle cleanup in useEffect properly?
- Optimize with useCallback and useMemo?
- Manage complex state with useReducer?
- Build a custom hook library for your project?
Unit Test Template
// __tests__/useCustomHook.test.js
import { renderHook, act, waitFor } from '@testing-library/react';
import { useCustomHook } from '../useCustomHook';
describe('useCustomHook', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useCustomHook());
expect(result.current.state).toBe(initialValue);
});
it('should handle async operations', async () => {
const { result } = renderHook(() => useCustomHook());
await act(async () => {
await result.current.asyncAction();
});
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
});
it('should cleanup on unmount', () => {
const cleanup = jest.fn();
const { unmount } = renderHook(() => useCustomHook({ onCleanup: cleanup }));
unmount();
expect(cleanup).toHaveBeenCalled();
});
});
Retry Logic Pattern
function useRetryAsync(asyncFn, options = {}) {
const { maxRetries = 3, backoff = 1000 } = options;
const [state, setState] = useState({ data: null, error: null, loading: false });
const execute = useCallback(async (...args) => {
setState(s => ({ ...s, loading: true, error: null }));
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const data = await asyncFn(...args);
setState({ data, error: null, loading: false });
return data;
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, backoff * Math.pow(2, attempt)));
}
}
}
setState({ data: null, error: lastError, loading: false });
throw lastError;
}, [asyncFn, maxRetries, backoff]);
return { ...state, execute };
}
Version: 2.0.0 Last Updated: 2025-12-30 SASMP Version: 2.0.0 Difficulty: Intermediate Estimated Time: 2-3 weeks Prerequisites: React Fundamentals Changelog: Added retry patterns, test templates, and observability hooks