Hooks are the foundation of modern React. Here's how to use them effectively and avoid common pitfalls.
useState#
1// Basic usage
2const [count, setCount] = useState(0);
3
4// Lazy initialization (expensive computation)
5const [data, setData] = useState(() => {
6 return computeExpensiveInitialValue();
7});
8
9// Functional updates (when new state depends on previous)
10setCount(prev => prev + 1);
11
12// Object state
13const [user, setUser] = useState({ name: '', email: '' });
14
15// Update single field
16setUser(prev => ({ ...prev, name: 'John' }));
17
18// Common mistake: stale closure
19function Counter() {
20 const [count, setCount] = useState(0);
21
22 useEffect(() => {
23 const interval = setInterval(() => {
24 // ❌ Always logs 0 (stale closure)
25 console.log(count);
26
27 // ❌ Always sets to 1
28 setCount(count + 1);
29
30 // ✓ Uses latest value
31 setCount(prev => prev + 1);
32 }, 1000);
33
34 return () => clearInterval(interval);
35 }, []); // Empty deps means closure captures initial values
36
37 return <div>{count}</div>;
38}useEffect#
1// Run on every render
2useEffect(() => {
3 console.log('Rendered');
4});
5
6// Run once on mount
7useEffect(() => {
8 fetchData();
9}, []);
10
11// Run when dependencies change
12useEffect(() => {
13 fetchUser(userId);
14}, [userId]);
15
16// Cleanup function
17useEffect(() => {
18 const subscription = subscribe(userId);
19
20 return () => {
21 subscription.unsubscribe();
22 };
23}, [userId]);
24
25// Async in useEffect
26useEffect(() => {
27 // ❌ Can't make useEffect async directly
28 // async () => { ... }
29
30 // ✓ Define async function inside
31 async function fetchData() {
32 const result = await api.getData();
33 setData(result);
34 }
35
36 fetchData();
37}, []);
38
39// With abort controller
40useEffect(() => {
41 const controller = new AbortController();
42
43 async function fetchData() {
44 try {
45 const response = await fetch(url, {
46 signal: controller.signal,
47 });
48 const data = await response.json();
49 setData(data);
50 } catch (error) {
51 if (error.name !== 'AbortError') {
52 setError(error);
53 }
54 }
55 }
56
57 fetchData();
58
59 return () => controller.abort();
60}, [url]);useMemo and useCallback#
1// useMemo: Memoize computed values
2const expensiveValue = useMemo(() => {
3 return computeExpensiveValue(a, b);
4}, [a, b]);
5
6// useCallback: Memoize functions
7const handleClick = useCallback(() => {
8 doSomething(a, b);
9}, [a, b]);
10
11// When to use useMemo
12function SearchResults({ query, items }: Props) {
13 // ✓ Expensive filtering
14 const filteredItems = useMemo(() => {
15 return items.filter(item =>
16 item.name.toLowerCase().includes(query.toLowerCase())
17 );
18 }, [query, items]);
19
20 // ❌ Don't memoize cheap operations
21 const upperQuery = query.toUpperCase(); // Just compute it
22
23 return <List items={filteredItems} />;
24}
25
26// When to use useCallback
27function Parent() {
28 const [count, setCount] = useState(0);
29
30 // Without useCallback, Child re-renders on every Parent render
31 const handleClick = useCallback(() => {
32 setCount(c => c + 1);
33 }, []);
34
35 return <MemoizedChild onClick={handleClick} />;
36}
37
38const MemoizedChild = React.memo(function Child({ onClick }) {
39 return <button onClick={onClick}>Click</button>;
40});useRef#
1// DOM reference
2function TextInput() {
3 const inputRef = useRef<HTMLInputElement>(null);
4
5 const focusInput = () => {
6 inputRef.current?.focus();
7 };
8
9 return <input ref={inputRef} />;
10}
11
12// Mutable value that persists across renders
13function Timer() {
14 const [count, setCount] = useState(0);
15 const intervalRef = useRef<number | null>(null);
16
17 useEffect(() => {
18 intervalRef.current = setInterval(() => {
19 setCount(c => c + 1);
20 }, 1000);
21
22 return () => {
23 if (intervalRef.current) {
24 clearInterval(intervalRef.current);
25 }
26 };
27 }, []);
28
29 const stop = () => {
30 if (intervalRef.current) {
31 clearInterval(intervalRef.current);
32 }
33 };
34
35 return (
36 <div>
37 {count}
38 <button onClick={stop}>Stop</button>
39 </div>
40 );
41}
42
43// Previous value
44function usePrevious<T>(value: T): T | undefined {
45 const ref = useRef<T>();
46
47 useEffect(() => {
48 ref.current = value;
49 }, [value]);
50
51 return ref.current;
52}
53
54function Counter() {
55 const [count, setCount] = useState(0);
56 const prevCount = usePrevious(count);
57
58 return (
59 <div>
60 Now: {count}, Before: {prevCount}
61 </div>
62 );
63}useReducer#
1interface State {
2 count: number;
3 error: string | null;
4}
5
6type Action =
7 | { type: 'increment' }
8 | { type: 'decrement' }
9 | { type: 'reset' }
10 | { type: 'setError'; error: string };
11
12function reducer(state: State, action: Action): State {
13 switch (action.type) {
14 case 'increment':
15 return { ...state, count: state.count + 1 };
16 case 'decrement':
17 return { ...state, count: state.count - 1 };
18 case 'reset':
19 return { count: 0, error: null };
20 case 'setError':
21 return { ...state, error: action.error };
22 default:
23 return state;
24 }
25}
26
27function Counter() {
28 const [state, dispatch] = useReducer(reducer, { count: 0, error: null });
29
30 return (
31 <div>
32 Count: {state.count}
33 <button onClick={() => dispatch({ type: 'increment' })}>+</button>
34 <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
35 <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
36 </div>
37 );
38}Custom Hooks#
1// Fetch hook
2function useFetch<T>(url: string) {
3 const [data, setData] = useState<T | null>(null);
4 const [loading, setLoading] = useState(true);
5 const [error, setError] = useState<Error | null>(null);
6
7 useEffect(() => {
8 const controller = new AbortController();
9
10 async function fetchData() {
11 try {
12 setLoading(true);
13 const response = await fetch(url, { signal: controller.signal });
14 const json = await response.json();
15 setData(json);
16 setError(null);
17 } catch (err) {
18 if (err.name !== 'AbortError') {
19 setError(err as Error);
20 }
21 } finally {
22 setLoading(false);
23 }
24 }
25
26 fetchData();
27
28 return () => controller.abort();
29 }, [url]);
30
31 return { data, loading, error };
32}
33
34// Local storage hook
35function useLocalStorage<T>(key: string, initialValue: T) {
36 const [storedValue, setStoredValue] = useState<T>(() => {
37 try {
38 const item = window.localStorage.getItem(key);
39 return item ? JSON.parse(item) : initialValue;
40 } catch {
41 return initialValue;
42 }
43 });
44
45 const setValue = (value: T | ((val: T) => T)) => {
46 const valueToStore = value instanceof Function ? value(storedValue) : value;
47 setStoredValue(valueToStore);
48 window.localStorage.setItem(key, JSON.stringify(valueToStore));
49 };
50
51 return [storedValue, setValue] as const;
52}
53
54// Debounce hook
55function useDebounce<T>(value: T, delay: number): T {
56 const [debouncedValue, setDebouncedValue] = useState(value);
57
58 useEffect(() => {
59 const timer = setTimeout(() => {
60 setDebouncedValue(value);
61 }, delay);
62
63 return () => clearTimeout(timer);
64 }, [value, delay]);
65
66 return debouncedValue;
67}
68
69// Usage
70function SearchInput() {
71 const [query, setQuery] = useState('');
72 const debouncedQuery = useDebounce(query, 300);
73
74 const { data, loading } = useFetch(
75 `/api/search?q=${encodeURIComponent(debouncedQuery)}`
76 );
77
78 return (
79 <div>
80 <input value={query} onChange={e => setQuery(e.target.value)} />
81 {loading ? <Spinner /> : <Results data={data} />}
82 </div>
83 );
84}Rules of Hooks#
1// ✓ Call at top level
2function Component() {
3 const [count, setCount] = useState(0);
4 const value = useMemo(() => compute(count), [count]);
5 // ...
6}
7
8// ❌ Don't call in conditions
9function BadComponent({ show }) {
10 if (show) {
11 const [count, setCount] = useState(0); // Error!
12 }
13}
14
15// ❌ Don't call in loops
16function BadComponent({ items }) {
17 items.forEach(item => {
18 const [value, setValue] = useState(item); // Error!
19 });
20}
21
22// ❌ Don't call in nested functions
23function BadComponent() {
24 function handleClick() {
25 const [count, setCount] = useState(0); // Error!
26 }
27}Best Practices#
Dependencies:
✓ Include all values used inside effect
✓ Use ESLint exhaustive-deps rule
✓ Use refs for values you don't want to track
✓ Consider useReducer for complex state
Performance:
✓ Memoize expensive computations
✓ Use useCallback for callbacks passed to children
✓ Split state by update frequency
✓ Don't over-optimize
Custom Hooks:
✓ Extract reusable logic
✓ Start with 'use' prefix
✓ Return consistent shapes
✓ Handle cleanup properly
Conclusion#
Hooks enable powerful patterns in React. Understand the rules, manage dependencies carefully, and create custom hooks for reusable logic. Use useMemo and useCallback judiciously—profile first before optimizing.