Effect Patterns and Best Practices
Now that you understand useEffect basics, let's explore advanced patterns, common pitfalls, and when to avoid effects altogether.
Effect Timing: Commit vs Paint
React provides two effect hooks with different timing:
jsx1import { useEffect, useLayoutEffect } from "react";23// useEffect: Runs AFTER paint (non-blocking)4useEffect(() => {5 // User sees the screen, then this runs6 // Good for: data fetching, subscriptions, logging7});89// useLayoutEffect: Runs BEFORE paint (blocking)10useLayoutEffect(() => {11 // This runs before user sees anything12 // Good for: measuring DOM, preventing flicker13 const height = ref.current.getBoundingClientRect().height;14 setHeight(height);15});
Rule: Use useEffect by default. Only use useLayoutEffect when you need to measure or mutate the DOM before paint.
Debouncing in Effects
Delay execution until user stops typing:
jsx1function SearchInput({ onSearch }) {2 const [query, setQuery] = useState("");34 useEffect(() => {5 // Don't search if empty6 if (!query) return;78 // Set up debounce timer9 const timeoutId = setTimeout(() => {10 onSearch(query);11 }, 300); // Wait 300ms after last keystroke1213 // Cleanup: cancel if query changes before timeout14 return () => clearTimeout(timeoutId);15 }, [query, onSearch]);1617 return (18 <input19 value={query}20 onChange={(e) => setQuery(e.target.value)}21 placeholder="Search..."22 />23 );24}
Each keystroke:
- Cancels the previous timeout (cleanup)
- Starts a new 300ms timeout
- Only fires search after 300ms of no typing
Throttling in Effects
Limit how often an effect can run:
jsx1function ScrollTracker() {2 const [scrollY, setScrollY] = useState(0);3 const lastUpdate = useRef(0);45 useEffect(() => {6 const handleScroll = () => {7 const now = Date.now();8 // Only update every 100ms9 if (now - lastUpdate.current >= 100) {10 setScrollY(window.scrollY);11 lastUpdate.current = now;12 }13 };1415 window.addEventListener("scroll", handleScroll);16 return () => window.removeEventListener("scroll", handleScroll);17 }, []);1819 return <p>Scroll position: {scrollY}px</p>;20}
Handling Stale Closures
Effects capture values from when they were created:
jsx1function Counter() {2 const [count, setCount] = useState(0);34 useEffect(() => {5 const id = setInterval(() => {6 // BUG: count is always 0 (captured at mount)7 console.log("Count:", count);8 setCount(count + 1); // Always sets to 1!9 }, 1000);1011 return () => clearInterval(id);12 }, []); // Empty deps = effect never re-runs1314 return <p>{count}</p>;15}
Approaches:
jsx1// Approach 1: Add count to dependencies (effect re-runs)2useEffect(() => {3 const id = setInterval(() => {4 setCount(count + 1);5 }, 1000);6 return () => clearInterval(id);7}, [count]); // Re-runs when count changes89// Approach 2: Functional update (preferred for this case)10useEffect(() => {11 const id = setInterval(() => {12 setCount((c) => c + 1); // Always uses latest value13 }, 1000);14 return () => clearInterval(id);15}, []); // No dependency needed1617// Approach 3: Use ref for latest value18const countRef = useRef(count);19countRef.current = count; // Update ref on every render2021useEffect(() => {22 const id = setInterval(() => {23 console.log("Count:", countRef.current); // Always current24 }, 1000);25 return () => clearInterval(id);26}, []);
Multiple Effects
Separate unrelated logic into multiple effects:
jsx1function UserDashboard({ userId }) {2 const [user, setUser] = useState(null);3 const [posts, setPosts] = useState([]);45 // Effect 1: Fetch user data6 useEffect(() => {7 fetchUser(userId).then(setUser);8 }, [userId]);910 // Effect 2: Fetch posts (separate concern)11 useEffect(() => {12 fetchPosts(userId).then(setPosts);13 }, [userId]);1415 // Effect 3: Update document title (different lifecycle)16 useEffect(() => {17 if (user) {18 document.title = `${user.name}'s Dashboard`;19 }20 }, [user]);2122 // ...23}
Benefits:
- Easier to understand each effect's purpose
- Different dependencies for different concerns
- Easier to extract into custom hooks
Synchronizing with External Systems
Effects are for synchronizing React with external systems:
jsx1// Sync with browser API2function useOnlineStatus() {3 const [isOnline, setIsOnline] = useState(navigator.onLine);45 useEffect(() => {6 const handleOnline = () => setIsOnline(true);7 const handleOffline = () => setIsOnline(false);89 window.addEventListener("online", handleOnline);10 window.addEventListener("offline", handleOffline);1112 return () => {13 window.removeEventListener("online", handleOnline);14 window.removeEventListener("offline", handleOffline);15 };16 }, []);1718 return isOnline;19}2021// Sync with WebSocket22function useChatRoom({ roomId }) {23 useEffect(() => {24 const connection = createConnection(roomId);25 connection.connect();2627 return () => connection.disconnect();28 }, [roomId]);29}3031// Sync with third-party library32function useMapMarker({ map, position }) {33 useEffect(() => {34 const marker = new google.maps.Marker({ position, map });3536 return () => marker.setMap(null);37 }, [map, position]);38}
Avoiding Effect Chains
Don't chain effects to transform data:
jsx1// BAD - Effect chains2function ProductPage({ productId }) {3 const [product, setProduct] = useState(null);4 const [category, setCategory] = useState(null);5 const [relatedProducts, setRelatedProducts] = useState(null);67 // Chain 1: Fetch product8 useEffect(() => {9 fetchProduct(productId).then(setProduct);10 }, [productId]);1112 // Chain 2: Fetch category when product loads13 useEffect(() => {14 if (product) {15 fetchCategory(product.categoryId).then(setCategory);16 }17 }, [product]);1819 // Chain 3: Fetch related when category loads20 useEffect(() => {21 if (category) {22 fetchRelated(category.id).then(setRelatedProducts);23 }24 }, [category]);25}2627// GOOD - Single effect with async/await28function ProductPage({ productId }) {29 const [data, setData] = useState(null);30 const [loading, setLoading] = useState(true);3132 useEffect(() => {33 async function loadData() {34 setLoading(true);3536 const product = await fetchProduct(productId);37 const category = await fetchCategory(product.categoryId);38 const related = await fetchRelated(category.id);3940 setData({ product, category, related });41 setLoading(false);42 }4344 loadData();45 }, [productId]);46}
Strict Mode and Double Effects
In development with Strict Mode, effects run twice:
jsx1// In StrictMode, this logs twice on mount:2// "Mounted"3// "Cleanup"4// "Mounted"56useEffect(() => {7 console.log("Mounted");8 return () => console.log("Cleanup");9}, []);
This helps find bugs with missing cleanup. Your effect should:
- Work correctly if run multiple times
- Clean up properly between runs
Common Mistakes
1. Object/Array Dependencies
jsx1// BAD - new object every render = infinite loop!2useEffect(() => {3 fetchData(options);4}, [{ page: 1 }]); // New object reference each time56// GOOD - use primitives or useMemo7const page = 1;8useEffect(() => {9 fetchData({ page });10}, [page]);
2. Missing Cleanup
jsx1// BAD - memory leak!2useEffect(() => {3 const handler = () => {4 /* ... */5 };6 window.addEventListener("resize", handler);7 // Missing: return () => window.removeEventListener...8}, []);
3. Fetching Without Handling Unmount
jsx1// BAD - state update after unmount2useEffect(() => {3 fetch("/api/data")4 .then((r) => r.json())5 .then(setData); // Component may have unmounted!6}, []);78// GOOD - check if still mounted9useEffect(() => {10 let mounted = true;1112 fetch("/api/data")13 .then((r) => r.json())14 .then((data) => {15 if (mounted) setData(data);16 });1718 return () => {19 mounted = false;20 };21}, []);
When to Use Effects
DO use effects for:
- Fetching data
- Setting up subscriptions
- Synchronizing with external systems
- Tracking analytics
- Managing focus
DON'T use effects for:
- Computing derived data (calculate during render)
- Handling user events (use event handlers)
- Initializing state (use lazy initial state)
- Resetting state when props change (use key)
Summary
Effect best practices:
- Debounce/throttle expensive operations
- Handle stale closures with functional updates or refs
- Separate unrelated effects by concern
- Avoid effect chains - batch related fetches
- Always clean up subscriptions and timers
- Handle unmounting in async operations
- Test with Strict Mode to catch bugs
- Don't overuse effects - many cases don't need them
Next, let's build a live search feature that applies these patterns!