useEffect manages side effects in React components. Here's how to use it correctly.
Basic Patterns#
1import { useEffect, useState } from 'react';
2
3// Run on every render
4useEffect(() => {
5 console.log('Runs after every render');
6});
7
8// Run once on mount
9useEffect(() => {
10 console.log('Runs once on mount');
11}, []);
12
13// Run when dependencies change
14useEffect(() => {
15 console.log('Runs when count changes');
16}, [count]);
17
18// Cleanup function
19useEffect(() => {
20 const subscription = subscribe();
21
22 return () => {
23 subscription.unsubscribe(); // Cleanup
24 };
25}, []);Data Fetching#
1function UserProfile({ userId }: { userId: string }) {
2 const [user, setUser] = useState<User | null>(null);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState<Error | null>(null);
5
6 useEffect(() => {
7 let isCancelled = false;
8
9 async function fetchUser() {
10 try {
11 setLoading(true);
12 setError(null);
13
14 const response = await fetch(`/api/users/${userId}`);
15 const data = await response.json();
16
17 if (!isCancelled) {
18 setUser(data);
19 }
20 } catch (err) {
21 if (!isCancelled) {
22 setError(err as Error);
23 }
24 } finally {
25 if (!isCancelled) {
26 setLoading(false);
27 }
28 }
29 }
30
31 fetchUser();
32
33 return () => {
34 isCancelled = true; // Prevent state updates after unmount
35 };
36 }, [userId]);
37
38 if (loading) return <Spinner />;
39 if (error) return <Error message={error.message} />;
40 if (!user) return null;
41
42 return <div>{user.name}</div>;
43}
44
45// With AbortController
46useEffect(() => {
47 const controller = new AbortController();
48
49 async function fetchData() {
50 try {
51 const response = await fetch('/api/data', {
52 signal: controller.signal,
53 });
54 const data = await response.json();
55 setData(data);
56 } catch (err) {
57 if (err.name !== 'AbortError') {
58 setError(err);
59 }
60 }
61 }
62
63 fetchData();
64
65 return () => controller.abort();
66}, []);Event Listeners#
1function useWindowSize() {
2 const [size, setSize] = useState({
3 width: window.innerWidth,
4 height: window.innerHeight,
5 });
6
7 useEffect(() => {
8 function handleResize() {
9 setSize({
10 width: window.innerWidth,
11 height: window.innerHeight,
12 });
13 }
14
15 window.addEventListener('resize', handleResize);
16
17 return () => {
18 window.removeEventListener('resize', handleResize);
19 };
20 }, []);
21
22 return size;
23}
24
25// With debounce
26function useWindowSizeDebounced(delay = 100) {
27 const [size, setSize] = useState({
28 width: window.innerWidth,
29 height: window.innerHeight,
30 });
31
32 useEffect(() => {
33 let timeoutId: NodeJS.Timeout;
34
35 function handleResize() {
36 clearTimeout(timeoutId);
37 timeoutId = setTimeout(() => {
38 setSize({
39 width: window.innerWidth,
40 height: window.innerHeight,
41 });
42 }, delay);
43 }
44
45 window.addEventListener('resize', handleResize);
46
47 return () => {
48 window.removeEventListener('resize', handleResize);
49 clearTimeout(timeoutId);
50 };
51 }, [delay]);
52
53 return size;
54}
55
56// Keyboard shortcuts
57function useKeyboardShortcut(key: string, callback: () => void) {
58 useEffect(() => {
59 function handleKeyDown(event: KeyboardEvent) {
60 if (event.key === key && (event.metaKey || event.ctrlKey)) {
61 event.preventDefault();
62 callback();
63 }
64 }
65
66 document.addEventListener('keydown', handleKeyDown);
67 return () => document.removeEventListener('keydown', handleKeyDown);
68 }, [key, callback]);
69}Subscriptions#
1// WebSocket connection
2function useWebSocket(url: string) {
3 const [messages, setMessages] = useState<Message[]>([]);
4 const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
5
6 useEffect(() => {
7 const ws = new WebSocket(url);
8
9 ws.onopen = () => setStatus('connected');
10 ws.onclose = () => setStatus('disconnected');
11 ws.onerror = () => setStatus('disconnected');
12
13 ws.onmessage = (event) => {
14 const message = JSON.parse(event.data);
15 setMessages((prev) => [...prev, message]);
16 };
17
18 return () => {
19 ws.close();
20 };
21 }, [url]);
22
23 return { messages, status };
24}
25
26// Observable subscription
27function useObservable<T>(observable: Observable<T>, initialValue: T) {
28 const [value, setValue] = useState(initialValue);
29
30 useEffect(() => {
31 const subscription = observable.subscribe(setValue);
32 return () => subscription.unsubscribe();
33 }, [observable]);
34
35 return value;
36}Timers#
1// Interval
2function useInterval(callback: () => void, delay: number | null) {
3 const savedCallback = useRef(callback);
4
5 // Remember the latest callback
6 useEffect(() => {
7 savedCallback.current = callback;
8 }, [callback]);
9
10 // Set up the interval
11 useEffect(() => {
12 if (delay === null) return;
13
14 const id = setInterval(() => savedCallback.current(), delay);
15 return () => clearInterval(id);
16 }, [delay]);
17}
18
19// Timeout
20function useTimeout(callback: () => void, delay: number | null) {
21 const savedCallback = useRef(callback);
22
23 useEffect(() => {
24 savedCallback.current = callback;
25 }, [callback]);
26
27 useEffect(() => {
28 if (delay === null) return;
29
30 const id = setTimeout(() => savedCallback.current(), delay);
31 return () => clearTimeout(id);
32 }, [delay]);
33}
34
35// Countdown timer
36function useCountdown(initialSeconds: number) {
37 const [seconds, setSeconds] = useState(initialSeconds);
38 const [isRunning, setIsRunning] = useState(false);
39
40 useEffect(() => {
41 if (!isRunning || seconds <= 0) return;
42
43 const id = setInterval(() => {
44 setSeconds((s) => s - 1);
45 }, 1000);
46
47 return () => clearInterval(id);
48 }, [isRunning, seconds]);
49
50 return {
51 seconds,
52 isRunning,
53 start: () => setIsRunning(true),
54 pause: () => setIsRunning(false),
55 reset: () => setSeconds(initialSeconds),
56 };
57}Document Effects#
1// Update document title
2function useDocumentTitle(title: string) {
3 useEffect(() => {
4 const previousTitle = document.title;
5 document.title = title;
6
7 return () => {
8 document.title = previousTitle;
9 };
10 }, [title]);
11}
12
13// Prevent scroll
14function usePreventScroll(prevent: boolean) {
15 useEffect(() => {
16 if (!prevent) return;
17
18 const originalStyle = window.getComputedStyle(document.body).overflow;
19 document.body.style.overflow = 'hidden';
20
21 return () => {
22 document.body.style.overflow = originalStyle;
23 };
24 }, [prevent]);
25}
26
27// Click outside
28function useClickOutside(
29 ref: RefObject<HTMLElement>,
30 handler: () => void
31) {
32 useEffect(() => {
33 function handleClick(event: MouseEvent) {
34 if (ref.current && !ref.current.contains(event.target as Node)) {
35 handler();
36 }
37 }
38
39 document.addEventListener('mousedown', handleClick);
40 return () => document.removeEventListener('mousedown', handleClick);
41 }, [ref, handler]);
42}Common Pitfalls#
1// ❌ Missing dependency
2function BadComponent({ userId }) {
3 const [user, setUser] = useState(null);
4
5 useEffect(() => {
6 fetchUser(userId).then(setUser);
7 }, []); // userId is missing!
8
9 return <div>{user?.name}</div>;
10}
11
12// ✅ Include all dependencies
13function GoodComponent({ userId }) {
14 const [user, setUser] = useState(null);
15
16 useEffect(() => {
17 fetchUser(userId).then(setUser);
18 }, [userId]); // userId included
19
20 return <div>{user?.name}</div>;
21}
22
23// ❌ Object/array in dependencies causes infinite loop
24function InfiniteLoop() {
25 const options = { page: 1 }; // New object every render!
26
27 useEffect(() => {
28 fetchData(options);
29 }, [options]); // Infinite loop!
30}
31
32// ✅ Memoize or use primitive values
33function Fixed() {
34 const page = 1;
35
36 useEffect(() => {
37 fetchData({ page });
38 }, [page]); // Primitive value is stable
39}
40
41// Or with useMemo
42function FixedWithMemo() {
43 const options = useMemo(() => ({ page: 1 }), []);
44
45 useEffect(() => {
46 fetchData(options);
47 }, [options]);
48}
49
50// ❌ Function in dependencies
51function FunctionDependency({ onSave }) {
52 useEffect(() => {
53 const id = setInterval(onSave, 5000);
54 return () => clearInterval(id);
55 }, [onSave]); // If parent doesn't memoize onSave, this runs every render
56}
57
58// ✅ Use ref for latest callback
59function FixedFunction({ onSave }) {
60 const onSaveRef = useRef(onSave);
61
62 useEffect(() => {
63 onSaveRef.current = onSave;
64 }, [onSave]);
65
66 useEffect(() => {
67 const id = setInterval(() => onSaveRef.current(), 5000);
68 return () => clearInterval(id);
69 }, []);
70}Sync vs Effects#
1// ❌ Using effect for derived state
2function Bad({ items }) {
3 const [total, setTotal] = useState(0);
4
5 useEffect(() => {
6 setTotal(items.reduce((sum, item) => sum + item.price, 0));
7 }, [items]);
8
9 return <div>Total: {total}</div>;
10}
11
12// ✅ Compute during render
13function Good({ items }) {
14 const total = items.reduce((sum, item) => sum + item.price, 0);
15
16 return <div>Total: {total}</div>;
17}
18
19// ❌ Effect for event handler logic
20function Bad() {
21 const [submitted, setSubmitted] = useState(false);
22
23 useEffect(() => {
24 if (submitted) {
25 sendAnalytics('form_submitted');
26 }
27 }, [submitted]);
28
29 return (
30 <form onSubmit={() => setSubmitted(true)}>
31 ...
32 </form>
33 );
34}
35
36// ✅ Put it in the event handler
37function Good() {
38 const handleSubmit = () => {
39 submitForm();
40 sendAnalytics('form_submitted');
41 };
42
43 return (
44 <form onSubmit={handleSubmit}>
45 ...
46 </form>
47 );
48}Testing Effects#
1import { renderHook, act, waitFor } from '@testing-library/react';
2
3test('useCounter increments', () => {
4 const { result } = renderHook(() => useCounter(0));
5
6 act(() => {
7 result.current.increment();
8 });
9
10 expect(result.current.count).toBe(1);
11});
12
13test('useFetch loads data', async () => {
14 const { result } = renderHook(() => useFetch('/api/data'));
15
16 expect(result.current.loading).toBe(true);
17
18 await waitFor(() => {
19 expect(result.current.loading).toBe(false);
20 });
21
22 expect(result.current.data).toBeDefined();
23});Best Practices#
Dependencies:
✓ Include all values used inside effect
✓ Use primitives when possible
✓ Memoize objects and functions
✓ Use refs for non-reactive values
Cleanup:
✓ Always clean up subscriptions
✓ Cancel pending requests
✓ Clear timers
✓ Remove event listeners
Structure:
✓ Keep effects focused
✓ Split unrelated logic
✓ Extract into custom hooks
✓ Avoid effects for derived state
Conclusion#
useEffect handles side effects like data fetching, subscriptions, and DOM manipulation. Always clean up effects, include all dependencies, and avoid common pitfalls like infinite loops. Extract reusable logic into custom hooks and compute derived state during render rather than in effects.