React Blitz Day 3: Effects, Data Fetching & Lifecycle
Master React useEffect hook, data fetching patterns, handling side effects, cleanup functions, and avoiding common pitfalls like infinite loops.
Duration: 1 hour
Prerequisites: Basic understanding of React components, useState, and TypeScript
Theory Part (20 minutes)
1. Understanding Side Effects
What are side effects in React?
Side effects are operations that interact with the outside world or affect something beyond the component’s scope. Examples include:
- Fetching data from an API
- Setting up subscriptions or timers
- Manually manipulating the DOM
- Logging to console
- Reading/writing to localStorage
Pure vs. Impure Functions
Pure functions:
- Always return the same output for the same input
- Have no side effects
- Don’t modify external state
// Pure function - always predictable
function add(a: number, b: number): number {
return a + b;
}
Impure functions:
- May produce different outputs for the same input
- Cause side effects
- Depend on or modify external state
// Impure function - depends on external state
let count = 0;
function increment(): number {
count++; // Modifies external state
return count;
}
When You Need useEffect
Use useEffect when you need to:
- Fetch data after component mounts
- Subscribe to external events
- Set up timers or intervals
- Synchronize with browser APIs (localStorage, DOM manipulation)
- Clean up resources when component unmounts
When You DON’T Need useEffect
Avoid useEffect for:
- Transforming data for rendering (use regular variables or useMemo)
- Handling user events (use event handlers)
- Calculating derived state (calculate during render)
- Updating state based on props (update during render)
// Bad - unnecessary useEffect
function SearchResults({ query }: { query: string }) {
const [filteredResults, setFilteredResults] = useState([]);
useEffect(() => {
setFilteredResults(results.filter(r => r.includes(query)));
}, [query]);
return <div>{/* render */}</div>;
}
// Good - calculate during render
function SearchResults({ query }: { query: string }) {
const filteredResults = results.filter(r => r.includes(query));
return <div>{/* render */}</div>;
}
2. The useEffect Hook
Basic Syntax
useEffect(() => {
// Side effect code here
return () => {
// Cleanup code here (optional)
};
}, [dependencies]);
Dependencies Array Variations
No dependencies array - runs after every render:
useEffect(() => {
console.log('Runs after every render');
});
Empty dependencies array - runs once on mount:
useEffect(() => {
console.log('Runs only once when component mounts');
}, []);
With dependencies - runs when dependencies change:
useEffect(() => {
console.log('Runs when userId changes');
}, [userId]);
Cleanup Functions
Cleanup functions run:
- Before the effect runs again
- When the component unmounts
useEffect(() => {
// Subscribe to something
const subscription = api.subscribe(userId);
// Cleanup function
return () => {
subscription.unsubscribe();
};
}, [userId]);
Common Pitfalls and Infinite Loops
Infinite loop - missing dependencies:
// Bad - infinite loop
useEffect(() => {
setCount(count + 1); // Updates state on every render
}); // No dependencies array
Infinite loop - dependency causes effect to run:
// Bad - infinite loop
useEffect(() => {
setData(fetchData()); // Creates new object
}, [data]); // data changes, effect runs, data changes again...
3. Data Fetching Patterns
Fetching Data with useEffect
Basic pattern for data fetching:
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
// Reset states when userId changes
setLoading(true);
setError(null);
fetch(`https://api.example.com/users/${userId}`)
.then(response => {
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user?.name}</div>;
}
Race Conditions and Cleanup
When fetching data based on changing props, you need to handle race conditions:
useEffect(() => {
let cancelled = false; // Flag to track if effect is stale
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => {
if (!cancelled) { // Only update if not cancelled
setUser(data);
}
});
return () => {
cancelled = true; // Mark as cancelled on cleanup
};
}, [userId]);
React 19’s use Hook for Promises
React 19 introduces the use hook for handling promises more elegantly:
import { use } from 'react';
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // Suspends until promise resolves
return <div>{user.name}</div>;
}
Introduction to Suspense for Data Fetching
Suspense allows you to declaratively handle loading states:
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile userPromise={fetchUser()} />
</Suspense>
);
}
Practice Part (35 minutes)
Exercise 1: Fetch and Display User Data (20 minutes)
Objective: Create a component that fetches and displays a list of users from the JSONPlaceholder API.
Requirements:
- Fetch data from
https://jsonplaceholder.typicode.com/users - Show loading state while fetching
- Display error message if fetch fails
- Show user list when data arrives
- Implement proper cleanup to avoid memory leaks
Solution:
import { useState, useEffect } from 'react';
// Define the User interface
interface User {
id: number;
name: string;
email: string;
username: string;
}
function UserList() {
// State for storing the list of users
const [users, setUsers] = useState<User[]>([]);
// State for tracking loading status
const [loading, setLoading] = useState<boolean>(true);
// State for storing any error that occurs
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Flag to track if the component is still mounted
let isMounted = true;
// Function to fetch users from the API
const fetchUsers = async () => {
try {
// Start loading
setLoading(true);
setError(null);
// Fetch data from JSONPlaceholder API
const response = await fetch('https://jsonplaceholder.typicode.com/users');
// Check if response is successful
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Parse JSON response
const data: User[] = await response.json();
// Only update state if component is still mounted
if (isMounted) {
setUsers(data);
setLoading(false);
}
} catch (err) {
// Handle any errors that occurred during fetch
if (isMounted) {
setError(err instanceof Error ? err.message : 'An error occurred');
setLoading(false);
}
}
};
// Call the fetch function
fetchUsers();
// Cleanup function - runs when component unmounts or effect re-runs
return () => {
isMounted = false; // Prevent state updates after unmount
};
}, []); // Empty dependency array - run only once on mount
// Render loading state
if (loading) {
return (
<div className="loading">
<p>Loading users...</p>
</div>
);
}
// Render error state
if (error) {
return (
<div className="error">
<p>Error: {error}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
);
}
// Render users list
return (
<div className="user-list">
<h2>Users ({users.length})</h2>
<ul>
{users.map(user => (
<li key={user.id}>
<div className="user-card">
<h3>{user.name}</h3>
<p>Username: @{user.username}</p>
<p>Email: {user.email}</p>
</div>
</li>
))}
</ul>
</div>
);
}
export default UserList;
Key Points:
- Used
isMountedflag to prevent state updates after component unmounts - Separated concerns: loading, error, and success states
- Used async/await for cleaner asynchronous code
- Proper error handling with try/catch
- Cleanup function prevents memory leaks
Exercise 2: Search with Debouncing (15 minutes)
Objective: Build a search component that fetches results as the user types, with debouncing to avoid excessive API calls.
Requirements:
- Input field for search terms
- Fetch results from
https://jsonplaceholder.typicode.com/users?name_like={searchTerm} - Implement debouncing (wait 500ms after typing stops)
- Show loading indicator during search
- Handle cleanup properly
Solution:
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
username: string;
}
function UserSearch() {
// State for the search input value
const [searchTerm, setSearchTerm] = useState<string>('');
// State for the debounced search term (used for API calls)
const [debouncedTerm, setDebouncedTerm] = useState<string>('');
// State for search results
const [results, setResults] = useState<User[]>([]);
// State for loading indicator
const [loading, setLoading] = useState<boolean>(false);
// State for error handling
const [error, setError] = useState<string | null>(null);
// Effect for debouncing the search term
useEffect(() => {
// Set up a timer to update debouncedTerm after 500ms
const timerId = setTimeout(() => {
setDebouncedTerm(searchTerm);
}, 500);
// Cleanup function - clear the timer if searchTerm changes
// This prevents the update if user types again within 500ms
return () => {
clearTimeout(timerId);
};
}, [searchTerm]); // Re-run when searchTerm changes
// Effect for fetching search results
useEffect(() => {
// Don't search if the term is empty or too short
if (debouncedTerm.length < 2) {
setResults([]);
return;
}
// Flag to track if the effect is still active
let isCancelled = false;
// Async function to fetch search results
const searchUsers = async () => {
try {
setLoading(true);
setError(null);
// Fetch all users (JSONPlaceholder doesn't support name_like, so we filter client-side)
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data: User[] = await response.json();
// Filter results based on search term
const filteredResults = data.filter(user =>
user.name.toLowerCase().includes(debouncedTerm.toLowerCase()) ||
user.username.toLowerCase().includes(debouncedTerm.toLowerCase())
);
// Only update state if effect hasn't been cancelled
if (!isCancelled) {
setResults(filteredResults);
setLoading(false);
}
} catch (err) {
if (!isCancelled) {
setError(err instanceof Error ? err.message : 'Search failed');
setLoading(false);
}
}
};
// Execute the search
searchUsers();
// Cleanup function - cancel the effect if debouncedTerm changes
return () => {
isCancelled = true;
};
}, [debouncedTerm]); // Re-run when debouncedTerm changes
return (
<div className="user-search">
<h2>Search Users</h2>
{/* Search input field */}
<input
type="text"
placeholder="Search by name or username..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
{/* Loading indicator */}
{loading && <p className="loading-text">Searching...</p>}
{/* Error message */}
{error && <p className="error-text">Error: {error}</p>}
{/* Search results */}
{!loading && searchTerm.length >= 2 && (
<div className="results">
<p>{results.length} result(s) found</p>
<ul>
{results.map(user => (
<li key={user.id}>
<div className="result-card">
<h3>{user.name}</h3>
<p>@{user.username}</p>
<p>{user.email}</p>
</div>
</li>
))}
</ul>
</div>
)}
{/* No results message */}
{!loading && searchTerm.length >= 2 && results.length === 0 && (
<p>No users found matching "{searchTerm}"</p>
)}
</div>
);
}
export default UserSearch;
Key Points:
- Two-stage state management:
searchTerm(immediate) anddebouncedTerm(delayed) - First useEffect implements debouncing with setTimeout
- Second useEffect performs the actual search
- Cleanup functions prevent race conditions and memory leaks
- Minimum search term length validation
- Client-side filtering (since JSONPlaceholder doesn’t support query parameters)
Quick Review - Key Concepts (5 minutes)
1. useEffect Runs After Render
Effects don’t block the browser from painting the screen. They run after the DOM has been updated, allowing React to keep the UI responsive.
function Component() {
console.log('1. Render');
useEffect(() => {
console.log('3. Effect runs');
});
console.log('2. Still rendering');
return <div>Hello</div>;
}
// Output: 1. Render -> 2. Still rendering -> 3. Effect runs
2. Dependencies Array Controls When Effects Run
| Dependencies | Behavior | Use Case |
|---|---|---|
| None | Runs after every render | Rarely needed, usually a mistake |
[] | Runs once on mount | Initial data fetching, subscriptions |
[dep1, dep2] | Runs when dependencies change | Fetch data based on props/state |
3. Always Clean Up
Return a cleanup function for:
- Timers and intervals
- Event listeners
- Subscriptions
- Pending API requests
- WebSocket connections
useEffect(() => {
const interval = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(interval); // Cleanup
}, []);
4. Don’t Overuse useEffect
Many scenarios don’t need useEffect:
// Bad - unnecessary effect
function Total({ price, quantity }: Props) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(price * quantity);
}, [price, quantity]);
return <div>{total}</div>;
}
// Good - calculate during render
function Total({ price, quantity }: Props) {
const total = price * quantity;
return <div>{total}</div>;
}
Checklist - What You Should Know After Day 3
- ✅ Use useEffect for side effects (data fetching, subscriptions, timers)
- ✅ Understand the dependencies array and when effects run
- ✅ Fetch data from APIs with proper error handling
- ✅ Handle loading and error states in your components
- ✅ Implement cleanup functions to prevent memory leaks
- ✅ Avoid common useEffect pitfalls (infinite loops, missing dependencies)
- ✅ Know when NOT to use useEffect (derived state, event handlers)
Quick Reference Card - Save This!
// Run once on mount
useEffect(() => {
fetchData();
}, []);
// Run when dependency changes
useEffect(() => {
fetchData(id);
}, [id]);
// Cleanup example
useEffect(() => {
const timer = setTimeout(() => {
console.log('Delayed action');
}, 1000);
// Cleanup function
return () => clearTimeout(timer);
}, []);
// Complete data fetching pattern
function DataComponent({ url }: { url: string }) {
const [data, setData] = useState<Data | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
// Cancellation flag
let cancelled = false;
// Reset states
setLoading(true);
setError(null);
// Fetch data
fetch(url)
.then(res => {
if (!res.ok) throw new Error('Fetch failed');
return res.json();
})
.then(data => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
// Cleanup function
return () => {
cancelled = true;
};
}, [url]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{/* render data */}</div>;
}
// Debouncing pattern
useEffect(() => {
const timerId = setTimeout(() => {
// Action after delay
performSearch(searchTerm);
}, 500);
return () => clearTimeout(timerId);
}, [searchTerm]);
Common Mistakes to Avoid
1. Missing Dependencies
// Bad - userId is used but not in dependencies
useEffect(() => {
fetchUser(userId);
}, []);
// Good - include all dependencies
useEffect(() => {
fetchUser(userId);
}, [userId]);
2. Forgetting Cleanup
// Bad - timer keeps running after unmount
useEffect(() => {
setInterval(() => console.log('tick'), 1000);
}, []);
// Good - cleanup the timer
useEffect(() => {
const id = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(id);
}, []);
3. Using Objects/Arrays as Dependencies
// Bad - object is recreated on every render
const config = { url: '/api/data' };
useEffect(() => {
fetch(config.url);
}, [config]); // Infinite loop!
// Good - use primitive values
useEffect(() => {
fetch(config.url);
}, [config.url]);
What’s Tomorrow?
On Day 4, we’ll dive into:
- Complex state management with
useReducer - Context API for global state
- Composition patterns and component architecture
- When to use Context vs. Props
- Performance optimization with Context
Additional Resources
- React Docs - useEffect
- React Docs - You Might Not Need an Effect
- React Docs - Synchronizing with Effects
Congratulations! You’ve completed Day 3 of the React course. You now understand how to handle side effects, fetch data from APIs, and manage component lifecycle with useEffect.