Master React hooks including useEffect for side effects, data fetching, subscriptions, and component lifecycle management. Learn modern React patterns and best practices.
React Hooks revolutionized how we write React components by allowing us to use state and other React features in functional components. The useEffect hook is particularly powerful - it handles side effects like data fetching, subscriptions, timers, and DOM manipulation.
Think of useEffect as a combination of componentDidMount, componentDidUpdate, and componentWillUnmount from class components, but with more precision and control. It lets you perform side effects in functional components while keeping your code clean and predictable.
Understanding useEffect is crucial for modern React development. It's the foundation for data fetching, real-time updates, performance optimization, and creating responsive, interactive applications that work seamlessly with external APIs and services.
In this comprehensive lesson, you'll master useEffect patterns, learn dependency management, implement real-world data fetching, and build interactive applications that demonstrate the full power of React hooks.
useEffect is React's hook for performing side effects in functional components. Side effects are operations that affect something outside the component's rendering - like API calls, timers, subscriptions, or DOM manipulation.
What are Side Effects?
Side effects are operations that:
Basic useEffect Syntax:
1import React, { useState, useEffect } from 'react';
2
3function UserProfile({ userId }) {
4 const [user, setUser] = useState(null);
5 const [loading, setLoading] = useState(true);
6
7 // useEffect runs after every render
8 useEffect(() => {
9 console.log('Effect runs after render');
10
11 // Side effect: fetch user data
12 fetchUserData(userId)
13 .then(userData => {
14 setUser(userData);
15 setLoading(false);
16 });
17 });
18
19 return (
20 <div>
21 {loading ? <p>Loading...</p> : <p>Hello, {user?.name}!</p>}
22 </div>
23 );
24}useEffect Timing:
useEffect runs after the DOM has been updated:
Why After Render?
Effect Without Dependencies:
1function Timer() {
2 const [count, setCount] = useState(0);
3
4 // Runs after EVERY render (potentially problematic)
5 useEffect(() => {
6 console.log('Effect runs every render');
7 document.title = `Count: ${count}`;
8 });
9
10 return (
11 <div>
12 <p>Count: {count}</p>
13 <button onClick={() => setCount(count + 1)}>Increment</button>
14 </div>
15 );
16}Common Side Effect Patterns:
1. Document/Browser APIs:
1function DocumentTitle({ title }) {
2 useEffect(() => {
3 document.title = title;
4 });
5
6 return <h1>{title}</h1>;
7}2. Data Fetching:
1function UserList() {
2 const [users, setUsers] = useState([]);
3
4 useEffect(() => {
5 fetch('/api/users')
6 .then(response => response.json())
7 .then(data => setUsers(data));
8 });
9
10 return (
11 <ul>
12 {users.map(user => <li key={user.id}>{user.name}</li>)}
13 </ul>
14 );
15}3. Event Listeners:
1function WindowSize() {
2 const [windowSize, setWindowSize] = useState({
3 width: window.innerWidth,
4 height: window.innerHeight
5 });
6
7 useEffect(() => {
8 function updateSize() {
9 setWindowSize({
10 width: window.innerWidth,
11 height: window.innerHeight
12 });
13 }
14
15 window.addEventListener('resize', updateSize);
16
17 // Cleanup function (important!)
18 return () => window.removeEventListener('resize', updateSize);
19 });
20
21 return <p>Window: {windowSize.width} x {windowSize.height}</p>;
22}Effect Execution Flow:
Key Concepts:
Understanding these fundamentals is crucial before learning dependency arrays and optimization patterns in the next section.
Dependency arrays are the key to controlling when useEffect runs. They prevent unnecessary re-runs, optimize performance, and ensure effects only execute when relevant data changes.
The Dependency Array Problem:
Without proper dependencies, effects can cause infinite loops or performance issues:
1// PROBLEMATIC: Infinite loop
2function BadExample() {
3 const [count, setCount] = useState(0);
4 const [users, setUsers] = useState([]);
5
6 useEffect(() => {
7 // This effect runs after every render
8 fetch('/api/users')
9 .then(response => response.json())
10 .then(data => setUsers(data)); // This causes re-render
11 // Effect runs again because component re-rendered
12 // Creates infinite loop!
13 }); // No dependency array
14
15 return <div>Count: {count}, Users: {users.length}</div>;
16}Dependency Array Solutions:
1. Empty Dependency Array - Run Once:
1function DataFetcher() {
2 const [users, setUsers] = useState([]);
3 const [loading, setLoading] = useState(true);
4
5 useEffect(() => {
6 console.log('Effect runs only once - like componentDidMount');
7
8 fetch('/api/users')
9 .then(response => response.json())
10 .then(data => {
11 setUsers(data);
12 setLoading(false);
13 });
14 }, []); // Empty array = run once on mount
15
16 return (
17 <div>
18 {loading ? 'Loading...' : `${users.length} users loaded`}
19 </div>
20 );
21}2. Specific Dependencies - Run When Values Change:
1function UserProfile({ userId }) {
2 const [user, setUser] = useState(null);
3 const [loading, setLoading] = useState(true);
4
5 useEffect(() => {
6 console.log(`Effect runs when userId changes: ${userId}`);
7
8 setLoading(true);
9 fetch(`/api/users/${userId}`)
10 .then(response => response.json())
11 .then(data => {
12 setUser(data);
13 setLoading(false);
14 });
15 }, [userId]); // Runs when userId changes
16
17 return (
18 <div>
19 {loading ? 'Loading...' : `User: ${user?.name}`}
20 </div>
21 );
22}3. Multiple Dependencies:
1function SearchResults({ query, category, sortBy }) {
2 const [results, setResults] = useState([]);
3
4 useEffect(() => {
5 console.log('Searching with:', { query, category, sortBy });
6
7 const searchParams = new URLSearchParams({
8 q: query,
9 category,
10 sort: sortBy
11 });
12
13 fetch(`/api/search?${searchParams}`)
14 .then(response => response.json())
15 .then(data => setResults(data));
16 }, [query, category, sortBy]); // Runs when any dependency changes
17
18 return <div>{results.length} search results</div>;
19}Dependency Array Rules:
Include ALL Values from Component Scope:
1function Timer({ initialCount, step }) {
2 const [count, setCount] = useState(initialCount);
3
4 useEffect(() => {
5 const interval = setInterval(() => {
6 // Uses 'step' from props - must be in dependencies
7 setCount(prevCount => prevCount + step);
8 }, 1000);
9
10 return () => clearInterval(interval);
11 }, [step]); // Include 'step' because it's used inside effect
12
13 return <div>Count: {count}</div>;
14}ESLint Plugin for Dependencies:
React provides an ESLint rule that warns about missing dependencies:
1// ESLint warning: "React Hook useEffect has a missing dependency: 'userId'"
2useEffect(() => {
3 fetchUser(userId); // Uses userId but not in dependencies
4}, []); // Should be [userId]Optimizing with useMemo and useCallback:
Sometimes dependencies change too often. Use useMemo/useCallback to stabilize them:
1function ExpensiveComponent({ items, filters }) {
2 // Memoize expensive calculations
3 const filteredItems = useMemo(() => {
4 return items.filter(item =>
5 filters.every(filter => filter(item))
6 );
7 }, [items, filters]);
8
9 useEffect(() => {
10 console.log('Process filtered items:', filteredItems.length);
11 // Do something with filtered items
12 }, [filteredItems]); // Now only runs when filteredItems actually change
13
14 return <div>{filteredItems.length} items</div>;
15}Advanced Dependency Patterns:
Function Dependencies:
1function SearchComponent({ onResults }) {
2 const [query, setQuery] = useState('');
3
4 // Wrap function in useCallback to stabilize reference
5 const handleSearch = useCallback(async () => {
6 const results = await searchAPI(query);
7 onResults(results);
8 }, [query, onResults]);
9
10 useEffect(() => {
11 if (query) {
12 handleSearch();
13 }
14 }, [handleSearch]); // Stable function reference
15
16 return (
17 <input
18 value={query}
19 onChange={(e) => setQuery(e.target.value)}
20 />
21 );
22}Object Dependencies (Avoid):
1// PROBLEMATIC: Object recreated every render
2function BadExample({ config }) {
3 useEffect(() => {
4 fetchData(config);
5 }, [config]); // Object reference changes every render
6}
7
8// BETTER: Destructure specific properties
9function GoodExample({ config }) {
10 const { apiUrl, timeout, retries } = config;
11
12 useEffect(() => {
13 fetchData({ apiUrl, timeout, retries });
14 }, [apiUrl, timeout, retries]); // Primitive values
15}Dependency Array Best Practices:
Understanding dependency arrays is crucial for building performant, bug-free React applications.
Data fetching is one of the most common use cases for useEffect. Modern React applications need to handle loading states, errors, race conditions, and cleanup properly.
Basic Data Fetching Pattern:
1function UserList() {
2 const [users, setUsers] = useState([]);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState(null);
5
6 useEffect(() => {
7 async function fetchUsers() {
8 try {
9 setLoading(true);
10 setError(null);
11
12 const response = await fetch('/api/users');
13 if (!response.ok) {
14 throw new Error(`HTTP error! status: ${response.status}`);
15 }
16
17 const userData = await response.json();
18 setUsers(userData);
19 } catch (err) {
20 setError(err.message);
21 } finally {
22 setLoading(false);
23 }
24 }
25
26 fetchUsers();
27 }, []); // Run once on mount
28
29 if (loading) return <div>Loading users...</div>;
30 if (error) return <div>Error: {error}</div>;
31
32 return (
33 <ul>
34 {users.map(user => (
35 <li key={user.id}>{user.name} - {user.email}</li>
36 ))}
37 </ul>
38 );
39}Advanced Loading States:
1function ProductCatalog({ category }) {
2 const [products, setProducts] = useState([]);
3 const [loading, setLoading] = useState(false);
4 const [error, setError] = useState(null);
5 const [hasMore, setHasMore] = useState(true);
6
7 useEffect(() => {
8 let cancelled = false; // Prevent state updates if component unmounts
9
10 async function fetchProducts() {
11 if (cancelled) return;
12
13 try {
14 setLoading(true);
15 setError(null);
16
17 const response = await fetch(`/api/products?category=${category}`);
18 const data = await response.json();
19
20 if (!cancelled) {
21 setProducts(data.products);
22 setHasMore(data.hasMore);
23 }
24 } catch (err) {
25 if (!cancelled) {
26 setError(err.message);
27 }
28 } finally {
29 if (!cancelled) {
30 setLoading(false);
31 }
32 }
33 }
34
35 fetchProducts();
36
37 // Cleanup: prevent state updates if component unmounts
38 return () => {
39 cancelled = true;
40 };
41 }, [category]); // Re-fetch when category changes
42
43 return (
44 <div>
45 <h2>Products - {category}</h2>
46 {loading && <div>Loading products...</div>}
47 {error && <div>Error: {error}</div>}
48
49 <div className="product-grid">
50 {products.map(product => (
51 <ProductCard key={product.id} product={product} />
52 ))}
53 </div>
54
55 {hasMore && !loading && (
56 <button onClick={() => {/* Load more logic */}}>
57 Load More
58 </button>
59 )}
60 </div>
61 );
62}Custom Hook for Data Fetching:
1// Custom hook: useApi
2function useApi(url) {
3 const [data, setData] = useState(null);
4 const [loading, setLoading] = useState(true);
5 const [error, setError] = useState(null);
6
7 useEffect(() => {
8 let cancelled = false;
9
10 async function fetchData() {
11 try {
12 setLoading(true);
13 setError(null);
14
15 const response = await fetch(url);
16 if (!response.ok) {
17 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
18 }
19
20 const result = await response.json();
21
22 if (!cancelled) {
23 setData(result);
24 }
25 } catch (err) {
26 if (!cancelled) {
27 setError(err.message);
28 }
29 } finally {
30 if (!cancelled) {
31 setLoading(false);
32 }
33 }
34 }
35
36 fetchData();
37
38 return () => {
39 cancelled = true;
40 };
41 }, [url]);
42
43 return { data, loading, error };
44}
45
46// Usage
47function UserProfile({ userId }) {
48 const { data: user, loading, error } = useApi(`/api/users/${userId}`);
49
50 if (loading) return <div>Loading...</div>;
51 if (error) return <div>Error: {error}</div>;
52 if (!user) return <div>User not found</div>;
53
54 return (
55 <div>
56 <h1>{user.name}</h1>
57 <p>{user.email}</p>
58 </div>
59 );
60}Handling Race Conditions:
1function SearchResults({ query }) {
2 const [results, setResults] = useState([]);
3 const [loading, setLoading] = useState(false);
4
5 useEffect(() => {
6 if (!query) {
7 setResults([]);
8 return;
9 }
10
11 let cancelled = false;
12 const controller = new AbortController();
13
14 async function performSearch() {
15 try {
16 setLoading(true);
17
18 const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
19 signal: controller.signal // Support request cancellation
20 });
21
22 const data = await response.json();
23
24 // Only update state if this is still the current search
25 if (!cancelled) {
26 setResults(data.results);
27 }
28 } catch (err) {
29 if (err.name !== 'AbortError' && !cancelled) {
30 console.error('Search failed:', err);
31 }
32 } finally {
33 if (!cancelled) {
34 setLoading(false);
35 }
36 }
37 }
38
39 performSearch();
40
41 return () => {
42 cancelled = true;
43 controller.abort(); // Cancel ongoing request
44 };
45 }, [query]);
46
47 return (
48 <div>
49 {loading && <div>Searching...</div>}
50 <div className="search-results">
51 {results.map(result => (
52 <SearchResultItem key={result.id} result={result} />
53 ))}
54 </div>
55 </div>
56 );
57}Pagination and Infinite Scroll:
1function InfiniteList() {
2 const [items, setItems] = useState([]);
3 const [page, setPage] = useState(1);
4 const [loading, setLoading] = useState(false);
5 const [hasMore, setHasMore] = useState(true);
6
7 useEffect(() => {
8 async function loadPage() {
9 if (!hasMore || loading) return;
10
11 setLoading(true);
12 try {
13 const response = await fetch(`/api/items?page=${page}&limit=20`);
14 const data = await response.json();
15
16 setItems(prev => [...prev, ...data.items]);
17 setHasMore(data.hasMore);
18 } catch (err) {
19 console.error('Failed to load page:', err);
20 } finally {
21 setLoading(false);
22 }
23 }
24
25 loadPage();
26 }, [page]); // Load when page changes
27
28 const loadMore = () => {
29 if (!loading && hasMore) {
30 setPage(prev => prev + 1);
31 }
32 };
33
34 return (
35 <div>
36 <div className="item-list">
37 {items.map(item => (
38 <ItemCard key={item.id} item={item} />
39 ))}
40 </div>
41
42 {loading && <div>Loading more...</div>}
43
44 {hasMore && !loading && (
45 <button onClick={loadMore}>Load More</button>
46 )}
47
48 {!hasMore && <div>No more items to load</div>}
49 </div>
50 );
51}Real-time Data with WebSockets:
1function LiveChat({ roomId }) {
2 const [messages, setMessages] = useState([]);
3 const [connectionStatus, setConnectionStatus] = useState('connecting');
4
5 useEffect(() => {
6 const ws = new WebSocket(`wss://api.example.com/chat/${roomId}`);
7
8 ws.onopen = () => {
9 setConnectionStatus('connected');
10 };
11
12 ws.onmessage = (event) => {
13 const message = JSON.parse(event.data);
14 setMessages(prev => [...prev, message]);
15 };
16
17 ws.onclose = () => {
18 setConnectionStatus('disconnected');
19 };
20
21 ws.onerror = (error) => {
22 console.error('WebSocket error:', error);
23 setConnectionStatus('error');
24 };
25
26 // Cleanup: close WebSocket when component unmounts
27 return () => {
28 ws.close();
29 };
30 }, [roomId]);
31
32 return (
33 <div>
34 <div>Status: {connectionStatus}</div>
35 <div className="messages">
36 {messages.map(message => (
37 <div key={message.id}>{message.text}</div>
38 ))}
39 </div>
40 </div>
41 );
42}Data Fetching Best Practices:
These patterns form the foundation of robust data fetching in React applications.
Cleanup functions are essential for preventing memory leaks, removing event listeners, cancelling subscriptions, and ensuring your React app performs well over time.
Why Cleanup is Important:
Without proper cleanup:
Basic Cleanup Pattern:
1function Timer() {
2 const [seconds, setSeconds] = useState(0);
3
4 useEffect(() => {
5 console.log('Setting up timer');
6
7 const interval = setInterval(() => {
8 setSeconds(prev => prev + 1);
9 }, 1000);
10
11 // Cleanup function: runs when component unmounts
12 // or before effect runs again
13 return () => {
14 console.log('Cleaning up timer');
15 clearInterval(interval);
16 };
17 }, []); // Empty dependency array = cleanup on unmount only
18
19 return <div>Timer: {seconds}s</div>;
20}Event Listener Cleanup:
1function WindowTracker() {
2 const [windowSize, setWindowSize] = useState({
3 width: window.innerWidth,
4 height: window.innerHeight
5 });
6
7 useEffect(() => {
8 function handleResize() {
9 setWindowSize({
10 width: window.innerWidth,
11 height: window.innerHeight
12 });
13 }
14
15 // Add event listener
16 window.addEventListener('resize', handleResize);
17
18 // Cleanup: remove event listener
19 return () => {
20 window.removeEventListener('resize', handleResize);
21 };
22 }, []); // No dependencies = setup once, cleanup on unmount
23
24 return (
25 <div>
26 Window size: {windowSize.width} x {windowSize.height}
27 </div>
28 );
29}Subscription Cleanup:
1function UserStatus({ userId }) {
2 const [status, setStatus] = useState('offline');
3
4 useEffect(() => {
5 // Subscribe to user status updates
6 const subscription = userStatusAPI.subscribe(userId, (newStatus) => {
7 setStatus(newStatus);
8 });
9
10 console.log(`Subscribed to user ${userId} status`);
11
12 // Cleanup: unsubscribe when component unmounts
13 // or when userId changes
14 return () => {
15 console.log(`Unsubscribing from user ${userId} status`);
16 subscription.unsubscribe();
17 };
18 }, [userId]); // Re-subscribe when userId changes
19
20 return <div>User status: {status}</div>;
21}Async Operation Cleanup:
1function UserProfile({ userId }) {
2 const [user, setUser] = useState(null);
3 const [loading, setLoading] = useState(true);
4
5 useEffect(() => {
6 let cancelled = false; // Flag to prevent state updates
7
8 async function fetchUser() {
9 try {
10 setLoading(true);
11 const response = await fetch(`/api/users/${userId}`);
12 const userData = await response.json();
13
14 // Only update state if component is still mounted
15 if (!cancelled) {
16 setUser(userData);
17 setLoading(false);
18 }
19 } catch (error) {
20 if (!cancelled) {
21 console.error('Failed to fetch user:', error);
22 setLoading(false);
23 }
24 }
25 }
26
27 fetchUser();
28
29 // Cleanup: prevent state updates if component unmounts
30 return () => {
31 cancelled = true;
32 };
33 }, [userId]);
34
35 if (loading) return <div>Loading...</div>;
36 return <div>User: {user?.name}</div>;
37}AbortController for Request Cancellation:
1function SearchComponent({ query }) {
2 const [results, setResults] = useState([]);
3 const [loading, setLoading] = useState(false);
4
5 useEffect(() => {
6 if (!query) {
7 setResults([]);
8 return;
9 }
10
11 const controller = new AbortController();
12
13 async function performSearch() {
14 try {
15 setLoading(true);
16 const response = await fetch(`/api/search?q=${query}`, {
17 signal: controller.signal // Attach abort signal
18 });
19
20 const data = await response.json();
21 setResults(data.results);
22 } catch (error) {
23 if (error.name !== 'AbortError') {
24 console.error('Search failed:', error);
25 }
26 } finally {
27 setLoading(false);
28 }
29 }
30
31 performSearch();
32
33 // Cleanup: abort the request if query changes or component unmounts
34 return () => {
35 controller.abort();
36 };
37 }, [query]);
38
39 return (
40 <div>
41 {loading && <div>Searching...</div>}
42 <SearchResults results={results} />
43 </div>
44 );
45}WebSocket Cleanup:
1function LiveNotifications({ userId }) {
2 const [notifications, setNotifications] = useState([]);
3 const [connectionStatus, setConnectionStatus] = useState('disconnected');
4
5 useEffect(() => {
6 const ws = new WebSocket(`wss://api.example.com/notifications/${userId}`);
7
8 ws.onopen = () => {
9 console.log('WebSocket connected');
10 setConnectionStatus('connected');
11 };
12
13 ws.onmessage = (event) => {
14 const notification = JSON.parse(event.data);
15 setNotifications(prev => [notification, ...prev]);
16 };
17
18 ws.onclose = () => {
19 console.log('WebSocket disconnected');
20 setConnectionStatus('disconnected');
21 };
22
23 ws.onerror = (error) => {
24 console.error('WebSocket error:', error);
25 setConnectionStatus('error');
26 };
27
28 // Cleanup: close WebSocket connection
29 return () => {
30 console.log('Closing WebSocket connection');
31 ws.close();
32 };
33 }, [userId]); // Reconnect when userId changes
34
35 return (
36 <div>
37 <div>Status: {connectionStatus}</div>
38 <NotificationList notifications={notifications} />
39 </div>
40 );
41}Complex Cleanup with Multiple Resources:
1function MediaPlayer({ videoId }) {
2 const [player, setPlayer] = useState(null);
3 const [progress, setProgress] = useState(0);
4
5 useEffect(() => {
6 // Create video player instance
7 const videoPlayer = new VideoPlayer({
8 containerId: 'video-container',
9 videoId: videoId
10 });
11
12 // Set up progress tracking
13 const progressInterval = setInterval(() => {
14 setProgress(videoPlayer.getCurrentTime());
15 }, 1000);
16
17 // Set up keyboard shortcuts
18 function handleKeyPress(event) {
19 if (event.key === ' ') {
20 event.preventDefault();
21 videoPlayer.togglePlay();
22 }
23 }
24
25 document.addEventListener('keydown', handleKeyPress);
26
27 setPlayer(videoPlayer);
28
29 // Complex cleanup: multiple resources
30 return () => {
31 console.log('Cleaning up media player');
32
33 // Clear interval
34 clearInterval(progressInterval);
35
36 // Remove event listeners
37 document.removeEventListener('keydown', handleKeyPress);
38
39 // Dispose of player resources
40 videoPlayer.destroy();
41 };
42 }, [videoId]); // Recreate player when videoId changes
43
44 return (
45 <div>
46 <div id="video-container"></div>
47 <div>Progress: {Math.round(progress)}s</div>
48 </div>
49 );
50}Custom Hook for Cleanup Patterns:
1// Custom hook for managing timers
2function useTimer(delay, callback, dependencies = []) {
3 useEffect(() => {
4 if (delay === null) return;
5
6 const timer = setInterval(callback, delay);
7
8 return () => clearInterval(timer);
9 }, [delay, callback, ...dependencies]);
10}
11
12// Custom hook for event listeners
13function useEventListener(eventName, handler, element = window) {
14 useEffect(() => {
15 const isSupported = element && element.addEventListener;
16 if (!isSupported) return;
17
18 element.addEventListener(eventName, handler);
19
20 return () => {
21 element.removeEventListener(eventName, handler);
22 };
23 }, [eventName, handler, element]);
24}
25
26// Usage
27function App() {
28 const [count, setCount] = useState(0);
29
30 // Auto-increment counter
31 useTimer(1000, () => setCount(c => c + 1));
32
33 // Handle keyboard shortcuts
34 useEventListener('keydown', (e) => {
35 if (e.key === 'r') setCount(0);
36 });
37
38 return <div>Count: {count} (Press 'r' to reset)</div>;
39}Cleanup Best Practices:
Proper cleanup ensures your React applications remain performant and stable over time.
Real-world applications combine multiple hooks to create complex, interactive experiences. Learn to build production-ready components that handle real user needs with proper state management, side effects, and user experience patterns.
Real-Time Dashboard Application:
1function Dashboard() {
2 const [metrics, setMetrics] = useState({
3 users: 0,
4 sales: 0,
5 revenue: 0,
6 errors: 0
7 });
8 const [timeRange, setTimeRange] = useState('24h');
9 const [isLive, setIsLive] = useState(true);
10 const [lastUpdate, setLastUpdate] = useState(new Date());
11
12 // Live data updates
13 useEffect(() => {
14 if (!isLive) return;
15
16 const interval = setInterval(async () => {
17 try {
18 const response = await fetch(`/api/metrics?range=${timeRange}`);
19 const data = await response.json();
20
21 setMetrics(data);
22 setLastUpdate(new Date());
23 } catch (error) {
24 console.error('Failed to fetch metrics:', error);
25 }
26 }, 5000); // Update every 5 seconds
27
28 return () => clearInterval(interval);
29 }, [timeRange, isLive]);
30
31 // Initial data load
32 useEffect(() => {
33 async function loadInitialData() {
34 try {
35 const response = await fetch(`/api/metrics?range=${timeRange}`);
36 const data = await response.json();
37 setMetrics(data);
38 } catch (error) {
39 console.error('Failed to load initial data:', error);
40 }
41 }
42
43 loadInitialData();
44 }, [timeRange]);
45
46 return (
47 <div className="dashboard">
48 <div className="dashboard-header">
49 <h1>Analytics Dashboard</h1>
50 <div className="controls">
51 <select
52 value={timeRange}
53 onChange={(e) => setTimeRange(e.target.value)}
54 >
55 <option value="1h">Last Hour</option>
56 <option value="24h">Last 24 Hours</option>
57 <option value="7d">Last 7 Days</option>
58 </select>
59
60 <button
61 onClick={() => setIsLive(!isLive)}
62 className={isLive ? 'live-active' : 'live-inactive'}
63 >
64 {isLive ? '🔴 Live' : '⏸️ Paused'}
65 </button>
66 </div>
67 </div>
68
69 <div className="metrics-grid">
70 <MetricCard title="Active Users" value={metrics.users} />
71 <MetricCard title="Sales" value={metrics.sales} />
72 <MetricCard title="Revenue" value={`$${metrics.revenue.toLocaleString()}`} />
73 <MetricCard title="Errors" value={metrics.errors} isError={metrics.errors > 10} />
74 </div>
75
76 <div className="dashboard-footer">
77 Last updated: {lastUpdate.toLocaleTimeString()}
78 </div>
79 </div>
80 );
81}
82
83function MetricCard({ title, value, isError = false }) {
84 return (
85 <div className={`metric-card ${isError ? 'error' : ''}`}>
86 <h3>{title}</h3>
87 <div className="metric-value">{value}</div>
88 </div>
89 );
90}Shopping Cart with Persistence:
1function ShoppingCart() {
2 const [cart, setCart] = useState([]);
3 const [products, setProducts] = useState([]);
4 const [loading, setLoading] = useState(true);
5
6 // Load cart from localStorage on mount
7 useEffect(() => {
8 const savedCart = localStorage.getItem('shopping-cart');
9 if (savedCart) {
10 setCart(JSON.parse(savedCart));
11 }
12 }, []);
13
14 // Save cart to localStorage whenever it changes
15 useEffect(() => {
16 localStorage.setItem('shopping-cart', JSON.stringify(cart));
17 }, [cart]);
18
19 // Load products
20 useEffect(() => {
21 async function loadProducts() {
22 try {
23 const response = await fetch('/api/products');
24 const data = await response.json();
25 setProducts(data);
26 } catch (error) {
27 console.error('Failed to load products:', error);
28 } finally {
29 setLoading(false);
30 }
31 }
32
33 loadProducts();
34 }, []);
35
36 const addToCart = (product) => {
37 setCart(prev => {
38 const existing = prev.find(item => item.id === product.id);
39 if (existing) {
40 return prev.map(item =>
41 item.id === product.id
42 ? { ...item, quantity: item.quantity + 1 }
43 : item
44 );
45 }
46 return [...prev, { ...product, quantity: 1 }];
47 });
48 };
49
50 const updateQuantity = (productId, newQuantity) => {
51 if (newQuantity <= 0) {
52 removeFromCart(productId);
53 return;
54 }
55
56 setCart(prev =>
57 prev.map(item =>
58 item.id === productId
59 ? { ...item, quantity: newQuantity }
60 : item
61 )
62 );
63 };
64
65 const removeFromCart = (productId) => {
66 setCart(prev => prev.filter(item => item.id !== productId));
67 };
68
69 const clearCart = () => {
70 setCart([]);
71 };
72
73 const cartTotal = cart.reduce((total, item) =>
74 total + (item.price * item.quantity), 0
75 );
76
77 const cartCount = cart.reduce((total, item) =>
78 total + item.quantity, 0
79 );
80
81 if (loading) return <div>Loading products...</div>;
82
83 return (
84 <div className="shopping-app">
85 <header className="app-header">
86 <h1>Shop</h1>
87 <div className="cart-summary">
88 Cart: {cartCount} items - ${cartTotal.toFixed(2)}
89 </div>
90 </header>
91
92 <div className="app-layout">
93 <div className="products-section">
94 <ProductGrid
95 products={products}
96 onAddToCart={addToCart}
97 cart={cart}
98 />
99 </div>
100
101 <div className="cart-section">
102 <Cart
103 items={cart}
104 onUpdateQuantity={updateQuantity}
105 onRemoveItem={removeFromCart}
106 onClearCart={clearCart}
107 total={cartTotal}
108 />
109 </div>
110 </div>
111 </div>
112 );
113}Chat Application with Real-Time Features:
1function ChatRoom({ roomId, userId }) {
2 const [messages, setMessages] = useState([]);
3 const [newMessage, setNewMessage] = useState('');
4 const [users, setUsers] = useState([]);
5 const [typing, setTyping] = useState([]);
6 const [connected, setConnected] = useState(false);
7 const messagesEndRef = useRef(null);
8
9 // WebSocket connection
10 useEffect(() => {
11 const ws = new WebSocket(`wss://api.example.com/chat/${roomId}`);
12
13 ws.onopen = () => {
14 setConnected(true);
15 ws.send(JSON.stringify({
16 type: 'join',
17 userId,
18 roomId
19 }));
20 };
21
22 ws.onmessage = (event) => {
23 const data = JSON.parse(event.data);
24
25 switch (data.type) {
26 case 'message':
27 setMessages(prev => [...prev, data.message]);
28 break;
29 case 'user_joined':
30 setUsers(prev => [...prev, data.user]);
31 break;
32 case 'user_left':
33 setUsers(prev => prev.filter(u => u.id !== data.userId));
34 break;
35 case 'typing_start':
36 setTyping(prev => [...prev.filter(id => id !== data.userId), data.userId]);
37 break;
38 case 'typing_stop':
39 setTyping(prev => prev.filter(id => id !== data.userId));
40 break;
41 default:
42 break;
43 }
44 };
45
46 ws.onclose = () => {
47 setConnected(false);
48 };
49
50 return () => {
51 ws.close();
52 };
53 }, [roomId, userId]);
54
55 // Auto-scroll to bottom when new messages arrive
56 useEffect(() => {
57 messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
58 }, [messages]);
59
60 // Handle typing indicators
61 useEffect(() => {
62 if (newMessage.trim()) {
63 // Send typing start
64 const ws = new WebSocket(`wss://api.example.com/chat/${roomId}`);
65 ws.send(JSON.stringify({
66 type: 'typing_start',
67 userId,
68 roomId
69 }));
70
71 const timeout = setTimeout(() => {
72 ws.send(JSON.stringify({
73 type: 'typing_stop',
74 userId,
75 roomId
76 }));
77 }, 3000);
78
79 return () => clearTimeout(timeout);
80 }
81 }, [newMessage, roomId, userId]);
82
83 const sendMessage = (e) => {
84 e.preventDefault();
85 if (!newMessage.trim() || !connected) return;
86
87 const ws = new WebSocket(`wss://api.example.com/chat/${roomId}`);
88 ws.send(JSON.stringify({
89 type: 'message',
90 message: {
91 text: newMessage,
92 userId,
93 timestamp: new Date().toISOString()
94 },
95 roomId
96 }));
97
98 setNewMessage('');
99 };
100
101 return (
102 <div className="chat-room">
103 <div className="chat-header">
104 <h2>Room: {roomId}</h2>
105 <div className="connection-status">
106 {connected ? '🟢 Connected' : '🔴 Disconnected'}
107 </div>
108 </div>
109
110 <div className="chat-layout">
111 <div className="messages-section">
112 <div className="messages-list">
113 {messages.map((message, index) => (
114 <Message key={index} message={message} isOwn={message.userId === userId} />
115 ))}
116 {typing.length > 0 && (
117 <div className="typing-indicator">
118 {typing.join(', ')} {typing.length === 1 ? 'is' : 'are'} typing...
119 </div>
120 )}
121 <div ref={messagesEndRef} />
122 </div>
123
124 <form onSubmit={sendMessage} className="message-form">
125 <input
126 type="text"
127 value={newMessage}
128 onChange={(e) => setNewMessage(e.target.value)}
129 placeholder="Type a message..."
130 disabled={!connected}
131 />
132 <button type="submit" disabled={!connected || !newMessage.trim()}>
133 Send
134 </button>
135 </form>
136 </div>
137
138 <div className="users-sidebar">
139 <h3>Users ({users.length})</h3>
140 <ul>
141 {users.map(user => (
142 <li key={user.id}>{user.name}</li>
143 ))}
144 </ul>
145 </div>
146 </div>
147 </div>
148 );
149}Search with Debouncing and Filters:
1function SearchInterface() {
2 const [query, setQuery] = useState('');
3 const [debouncedQuery, setDebouncedQuery] = useState('');
4 const [results, setResults] = useState([]);
5 const [filters, setFilters] = useState({
6 category: 'all',
7 sortBy: 'relevance',
8 dateRange: 'any'
9 });
10 const [loading, setLoading] = useState(false);
11 const [totalResults, setTotalResults] = useState(0);
12
13 // Debounce search query
14 useEffect(() => {
15 const timer = setTimeout(() => {
16 setDebouncedQuery(query);
17 }, 300);
18
19 return () => clearTimeout(timer);
20 }, [query]);
21
22 // Perform search when debounced query or filters change
23 useEffect(() => {
24 if (!debouncedQuery.trim()) {
25 setResults([]);
26 setTotalResults(0);
27 return;
28 }
29
30 let cancelled = false;
31
32 async function performSearch() {
33 setLoading(true);
34
35 try {
36 const params = new URLSearchParams({
37 q: debouncedQuery,
38 category: filters.category,
39 sort: filters.sortBy,
40 date: filters.dateRange
41 });
42
43 const response = await fetch(`/api/search?${params}`);
44 const data = await response.json();
45
46 if (!cancelled) {
47 setResults(data.results);
48 setTotalResults(data.total);
49 }
50 } catch (error) {
51 if (!cancelled) {
52 console.error('Search failed:', error);
53 }
54 } finally {
55 if (!cancelled) {
56 setLoading(false);
57 }
58 }
59 }
60
61 performSearch();
62
63 return () => {
64 cancelled = true;
65 };
66 }, [debouncedQuery, filters]);
67
68 const updateFilter = (key, value) => {
69 setFilters(prev => ({ ...prev, [key]: value }));
70 };
71
72 return (
73 <div className="search-interface">
74 <div className="search-header">
75 <input
76 type="text"
77 value={query}
78 onChange={(e) => setQuery(e.target.value)}
79 placeholder="Search..."
80 className="search-input"
81 />
82
83 <div className="search-filters">
84 <select
85 value={filters.category}
86 onChange={(e) => updateFilter('category', e.target.value)}
87 >
88 <option value="all">All Categories</option>
89 <option value="products">Products</option>
90 <option value="articles">Articles</option>
91 <option value="users">Users</option>
92 </select>
93
94 <select
95 value={filters.sortBy}
96 onChange={(e) => updateFilter('sortBy', e.target.value)}
97 >
98 <option value="relevance">Relevance</option>
99 <option value="date">Date</option>
100 <option value="popularity">Popularity</option>
101 </select>
102 </div>
103 </div>
104
105 <div className="search-results">
106 {loading && <div>Searching...</div>}
107
108 {!loading && debouncedQuery && (
109 <div className="results-summary">
110 {totalResults} results for "{debouncedQuery}"
111 </div>
112 )}
113
114 <div className="results-list">
115 {results.map(result => (
116 <SearchResult key={result.id} result={result} />
117 ))}
118 </div>
119 </div>
120 </div>
121 );
122}Key Patterns for Real-World Applications:
These real-world examples demonstrate how hooks work together to create production-ready React applications.
Explore React hooks and effects with real-world examples including useEffect patterns, data fetching, cleanup functions, and building interactive applications.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>React Hooks & Effects - Interactive Editor</title>
7 <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
8 <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
9 <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
10</head>
11<body>
12 <div id="root"></div>
13
14 <script type="text/babel">
15 const { useState, useEffect, useRef } = React;
16
17 // Timer Component - Basic useEffect
18 function Timer({ isActive }) {
19 const [seconds, setSeconds] = useState(0);
20
21 useEffect(() => {
22 let interval = null;
23
24 if (isActive) {
25 interval = setInterval(() => {
26 setSeconds(prev => prev + 1);
27 }, 1000);
28 }
29
30 // Cleanup function
31 return () => {
32 if (interval) {
33 clearInterval(interval);
34 }
35 };
36 }, [isActive]);
37
38 const formatTime = (totalSeconds) => {
39 const mins = Math.floor(totalSeconds / 60);
40 const secs = totalSeconds % 60;
41 return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
42 };
43
44 return (
45 <div className="timer">
46 <h3>Timer Demo</h3>
47 <div className="time-display">{formatTime(seconds)}</div>
48 <p className="timer-status">
49 Status: {isActive ? '🟢 Running' : '⏸️ Paused'}
50 </p>
51 </div>
52 );
53 }
54
55 // Data Fetcher Component - useEffect with async
56 function UserList() {
57 const [users, setUsers] = useState([]);
58 const [loading, setLoading] = useState(true);
59 const [error, setError] = useState(null);
60
61 useEffect(() => {
62 let cancelled = false;
63
64 async function fetchUsers() {
65 try {
66 setLoading(true);
67 setError(null);
68
69 // Simulate API call
70 await new Promise(resolve => setTimeout(resolve, 1000));
71
72 // Mock data
73 const mockUsers = [
74 { id: 1, name: 'Alice Johnson', email: '[email protected]', status: 'online' },
75 { id: 2, name: 'Bob Smith', email: '[email protected]', status: 'offline' },
76 { id: 3, name: 'Carol Davis', email: '[email protected]', status: 'online' },
77 { id: 4, name: 'David Wilson', email: '[email protected]', status: 'away' }
78 ];
79
80 if (!cancelled) {
81 setUsers(mockUsers);
82 setLoading(false);
83 }
84 } catch (err) {
85 if (!cancelled) {
86 setError(err.message);
87 setLoading(false);
88 }
89 }
90 }
91
92 fetchUsers();
93
94 return () => {
95 cancelled = true;
96 };
97 }, []);
98
99 if (loading) return <div className="loading">Loading users...</div>;
100 if (error) return <div className="error">Error: {error}</div>;
101
102 return (
103 <div className="user-list">
104 <h3>User List ({users.length} users)</h3>
105 {users.map(user => (
106 <div key={user.id} className="user-item">
107 <div className="user-info">
108 <h4>{user.name}</h4>
109 <p>{user.email}</p>
110 </div>
111 <span className={`status status-${user.status}`}>
112 {user.status}
113 </span>
114 </div>
115 ))}
116 </div>
117 );
118 }
119
120 // Window Size Tracker - Event listeners
121 function WindowTracker() {
122 const [windowSize, setWindowSize] = useState({
123 width: window.innerWidth,
124 height: window.innerHeight
125 });
126
127 useEffect(() => {
128 function handleResize() {
129 setWindowSize({
130 width: window.innerWidth,
131 height: window.innerHeight
132 });
133 }
134
135 window.addEventListener('resize', handleResize);
136
137 return () => {
138 window.removeEventListener('resize', handleResize);
139 };
140 }, []);
141
142 return (
143 <div className="window-tracker">
144 <h3>Window Size Tracker</h3>
145 <div className="size-info">
146 <div>Width: {windowSize.width}px</div>
147 <div>Height: {windowSize.height}px</div>
148 <div>Ratio: {(windowSize.width / windowSize.height).toFixed(2)}</div>
149 </div>
150 <p className="resize-hint">Resize your browser window to see live updates!</p>
151 </div>
152 );
153 }
154
155 // Live Search Component - Debouncing
156 function LiveSearch() {
157 const [query, setQuery] = useState('');
158 const [debouncedQuery, setDebouncedQuery] = useState('');
159 const [results, setResults] = useState([]);
160 const [searching, setSearching] = useState(false);
161
162 // Debounce the search query
163 useEffect(() => {
164 const timer = setTimeout(() => {
165 setDebouncedQuery(query);
166 }, 300);
167
168 return () => clearTimeout(timer);
169 }, [query]);
170
171 // Perform search when debounced query changes
172 useEffect(() => {
173 if (!debouncedQuery.trim()) {
174 setResults([]);
175 return;
176 }
177
178 let cancelled = false;
179
180 async function performSearch() {
181 setSearching(true);
182
183 // Simulate search API call
184 await new Promise(resolve => setTimeout(resolve, 500));
185
186 if (!cancelled) {
187 // Mock search results
188 const mockResults = [
189 `React ${debouncedQuery} tutorial`,
190 `Advanced ${debouncedQuery} patterns`,
191 `${debouncedQuery} best practices`,
192 `Building apps with ${debouncedQuery}`,
193 `${debouncedQuery} hooks guide`
194 ].filter(result =>
195 result.toLowerCase().includes(debouncedQuery.toLowerCase())
196 );
197
198 setResults(mockResults);
199 setSearching(false);
200 }
201 }
202
203 performSearch();
204
205 return () => {
206 cancelled = true;
207 };
208 }, [debouncedQuery]);
209
210 return (
211 <div className="live-search">
212 <h3>Live Search Demo</h3>
213 <div className="search-box">
214 <input
215 type="text"
216 value={query}
217 onChange={(e) => setQuery(e.target.value)}
218 placeholder="Search for anything..."
219 className="search-input"
220 />
221 {searching && <div className="search-spinner">🔍</div>}
222 </div>
223
224 {results.length > 0 && (
225 <div className="search-results">
226 <h4>Results for "{debouncedQuery}":</h4>
227 <ul>
228 {results.map((result, index) => (
229 <li key={index} className="search-result">
230 {result}
231 </li>
232 ))}
233 </ul>
234 </div>
235 )}
236 </div>
237 );
238 }
239
240 // Mouse Position Tracker
241 function MouseTracker() {
242 const [position, setPosition] = useState({ x: 0, y: 0 });
243 const [tracking, setTracking] = useState(false);
244
245 useEffect(() => {
246 if (!tracking) return;
247
248 function updatePosition(e) {
249 setPosition({ x: e.clientX, y: e.clientY });
250 }
251
252 document.addEventListener('mousemove', updatePosition);
253
254 return () => {
255 document.removeEventListener('mousemove', updatePosition);
256 };
257 }, [tracking]);
258
259 return (
260 <div className="mouse-tracker">
261 <h3>Mouse Position Tracker</h3>
262 <button
263 onClick={() => setTracking(!tracking)}
264 className={tracking ? 'btn-active' : 'btn-inactive'}
265 >
266 {tracking ? 'Stop Tracking' : 'Start Tracking'}
267 </button>
268 {tracking && (
269 <div className="position-info">
270 <div>X: {position.x}px</div>
271 <div>Y: {position.y}px</div>
272 </div>
273 )}
274 </div>
275 );
276 }
277
278 // Main App Component
279 function App() {
280 const [timerActive, setTimerActive] = useState(false);
281 const [activeDemo, setActiveDemo] = useState('timer');
282
283 const demos = [
284 { id: 'timer', name: 'Timer & Cleanup' },
285 { id: 'users', name: 'Data Fetching' },
286 { id: 'window', name: 'Event Listeners' },
287 { id: 'search', name: 'Debounced Search' },
288 { id: 'mouse', name: 'Mouse Tracking' }
289 ];
290
291 const renderActiveDemo = () => {
292 switch (activeDemo) {
293 case 'timer':
294 return (
295 <div>
296 <Timer isActive={timerActive} />
297 <button
298 onClick={() => setTimerActive(!timerActive)}
299 className="demo-button"
300 >
301 {timerActive ? 'Pause Timer' : 'Start Timer'}
302 </button>
303 </div>
304 );
305 case 'users':
306 return <UserList />;
307 case 'window':
308 return <WindowTracker />;
309 case 'search':
310 return <LiveSearch />;
311 case 'mouse':
312 return <MouseTracker />;
313 default:
314 return <div>Select a demo</div>;
315 }
316 };
317
318 return (
319 <div className="app">
320 <div className="header">
321 <h1>⚛️ React Hooks & Effects Playground</h1>
322 <p>Interactive examples of useEffect patterns and real-world use cases</p>
323 </div>
324
325 <div className="demo-selector">
326 <h2>Choose a Demo:</h2>
327 <div className="demo-buttons">
328 {demos.map(demo => (
329 <button
330 key={demo.id}
331 onClick={() => setActiveDemo(demo.id)}
332 className={activeDemo === demo.id ? 'demo-btn active' : 'demo-btn'}
333 >
334 {demo.name}
335 </button>
336 ))}
337 </div>
338 </div>
339
340 <div className="demo-container">
341 {renderActiveDemo()}
342 </div>
343
344 <div className="concepts-explained">
345 <h2>🎯 useEffect Concepts Demonstrated</h2>
346 <ul>
347 <li><strong>Timer Demo:</strong> Cleanup functions, conditional effects</li>
348 <li><strong>Data Fetching:</strong> Async operations, loading states, error handling</li>
349 <li><strong>Event Listeners:</strong> Adding/removing listeners, window events</li>
350 <li><strong>Debounced Search:</strong> Multiple useEffect hooks, debouncing patterns</li>
351 <li><strong>Mouse Tracking:</strong> Conditional subscriptions, real-time updates</li>
352 </ul>
353 </div>
354 </div>
355 );
356 }
357
358 ReactDOM.render(<App />, document.getElementById('root'));
359 </script>
360</body>
361</html>This comprehensive React Hooks & Effects playground demonstrates essential useEffect patterns:
🎯 Key Concepts Covered:
1. Timer & Cleanup Demo:
2. Data Fetching Demo:
3. Event Listeners Demo:
4. Debounced Search Demo:
5. Mouse Tracking Demo:
🔧 Advanced Patterns:
Cleanup Functions:
Dependency Arrays:
Async Handling:
Real-World Applications:
Performance Considerations:
This playground provides hands-on experience with the most important useEffect patterns you'll use in real React applications!
Excellent work mastering React hooks and effects! You now understand the foundation of modern React development with useState and useEffect. In our next lesson, we'll explore Vue.js Introduction & Templates, where you'll learn Vue's template syntax, directives, data binding, and compare Vue's approach to React's component patterns. Get ready to expand your framework knowledge!
Your progress is saved in this browser, on this device only.