What are Side Effects?
Side effects are operations that reach outside your component - like fetching data, updating the DOM, setting up subscriptions, or timers. useEffect lets you perform these operations at the right time.
Common Side Effects:
- Fetching data from APIs
- Subscribing to events
- Setting up timers/intervals
- Manually changing the DOM
- Logging/analytics
- Local storage operations
๐ Basic Syntax
import { useEffect } from 'react';
function Component() {
useEffect(() => {
// Side effect code here
console.log('Effect ran!');
// Optional: cleanup function
return () => {
console.log('Cleanup!');
};
}, [/* dependencies */]);
}
Three Forms of useEffect:
// 1. Runs after EVERY render
useEffect(() => {
console.log('Runs after every render');
});
// 2. Runs ONCE on mount (empty dependency array)
useEffect(() => {
console.log('Runs once on mount');
}, []);
// 3. Runs when dependencies change
useEffect(() => {
console.log('Runs when count changes');
}, [count]);
๐ฏ Dependency Array
The dependency array controls when your effect runs:
function Example() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Runs on every render (no dependency array)
useEffect(() => {
console.log('Every render');
});
// Runs once on mount (empty array)
useEffect(() => {
console.log('Component mounted');
}, []);
// Runs when count changes
useEffect(() => {
console.log('Count changed:', count);
}, [count]);
// Runs when either count OR name changes
useEffect(() => {
console.log('Count or name changed');
}, [count, name]);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
Rule: Include ALL Used Values
If your effect uses props, state, or functions, include them in the dependency array. ESLint will warn you if you forget!
๐งน Cleanup Functions
Return a cleanup function to prevent memory leaks:
// Timer cleanup
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup: clear interval when component unmounts
return () => clearInterval(interval);
}, []);
return <div>Seconds: {seconds}</div>;
}
// Event listener cleanup
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// Cleanup: remove listener
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>Width: {width}px</div>;
}
// Subscription cleanup
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const subscription = subscribeToRoom(roomId, (message) => {
setMessages(msgs => [...msgs, message]);
});
// Cleanup: unsubscribe
return () => subscription.unsubscribe();
}, [roomId]);
return <div>{/* render messages */}</div>;
}
๐ Fetching Data
Most common use case - fetch data from APIs:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Reset state when userId changes
setLoading(true);
setError(null);
// Fetch user data
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.message);
setLoading(false);
});
}, [userId]); // Re-fetch when userId changes
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
With Async/Await:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Can't make useEffect callback async directly
// Create async function inside
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
// ... rest of component
}
๐ Cleanup for Fetch Requests
Prevent race conditions and memory leaks:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
// Flag to track if component is still mounted
let cancelled = false;
const searchAPI = async () => {
setLoading(true);
try {
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
// Only update state if component is still mounted
if (!cancelled) {
setResults(data);
setLoading(false);
}
} catch (error) {
if (!cancelled) {
console.error(error);
setLoading(false);
}
}
};
searchAPI();
// Cleanup: mark as cancelled
return () => {
cancelled = true;
};
}, [query]);
return (
<div>
{loading && <p>Searching...</p>}
{results.map(result => <div key={result.id}>{result.title}</div>)}
</div>
);
}
Using AbortController (Modern Approach):
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, {
signal: controller.signal
})
.then(res => res.json())
.then(data => setResults(data))
.catch(error => {
if (error.name !== 'AbortError') {
console.error(error);
}
});
// Cleanup: abort request
return () => controller.abort();
}, [query]);
return <div>{/* render results */}</div>;
}
๐พ Syncing with localStorage
function usePersistentState(key, initialValue) {
// Load from localStorage
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved !== null ? JSON.parse(saved) : initialValue;
});
// Save to localStorage whenever value changes
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function TodoApp() {
const [todos, setTodos] = usePersistentState('todos', []);
// Todos automatically saved to localStorage!
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text, done: false }]);
};
return <div>{/* render todos */}</div>;
}
๐ฏ Common Patterns
1. Document Title:
function PageTitle({ title }) {
useEffect(() => {
document.title = title;
}, [title]);
return <div>{/* page content */}</div>;
}
// Custom hook version
function useDocumentTitle(title) {
useEffect(() => {
const prevTitle = document.title;
document.title = title;
// Restore previous title on unmount
return () => {
document.title = prevTitle;
};
}, [title]);
}
// Usage
function HomePage() {
useDocumentTitle('Home - My App');
return <div>Home Page</div>;
}
2. Debounced Search:
function SearchBox() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
// Don't search for empty term
if (!searchTerm) {
setResults([]);
return;
}
// Delay search by 500ms
const timeoutId = setTimeout(() => {
fetch(`/api/search?q=${searchTerm}`)
.then(res => res.json())
.then(setResults);
}, 500);
// Cleanup: cancel previous timeout
return () => clearTimeout(timeoutId);
}, [searchTerm]);
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
{results.map(r => <div key={r.id}>{r.name}</div>)}
</div>
);
}
3. WebSocket Connection:
function LiveFeed() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/live');
ws.onopen = () => {
console.log('Connected');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Cleanup: close connection
return () => {
ws.close();
};
}, []); // Empty array = connect once on mount
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.text}</div>
))}
</div>
);
}
๐ฏ Complete Example: Weather App
function WeatherApp() {
const [city, setCity] = useState('London');
const [weather, setWeather] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [lastUpdated, setLastUpdated] = useState(null);
// Fetch weather data
useEffect(() => {
const controller = new AbortController();
const fetchWeather = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_API_KEY`,
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error('City not found');
}
const data = await response.json();
setWeather(data);
setLastUpdated(new Date());
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchWeather();
return () => controller.abort();
}, [city]);
// Auto-refresh every 5 minutes
useEffect(() => {
const interval = setInterval(() => {
setCity(c => c); // Trigger re-fetch
}, 5 * 60 * 1000);
return () => clearInterval(interval);
}, []);
// Update document title
useEffect(() => {
if (weather) {
document.title = `${weather.main.temp}ยฐC - ${city}`;
}
}, [weather, city]);
return (
<div className="weather-app">
<input
value={city}
onChange={e => setCity(e.target.value)}
placeholder="Enter city"
/>
{loading && <div>Loading weather...</div>}
{error && (
<div className="error">
Error: {error}
</div>
)}
{weather && !loading && (
<div className="weather-info">
<h2>{weather.name}</h2>
<p>Temperature: {Math.round(weather.main.temp - 273.15)}ยฐC</p>
<p>Condition: {weather.weather[0].description}</p>
<p>Humidity: {weather.main.humidity}%</p>
{lastUpdated && (
<small>Last updated: {lastUpdated.toLocaleTimeString()}</small>
)}
</div>
)}
</div>
);
}
โ ๏ธ Common Mistakes
1. Missing Dependencies
// โ Wrong - missing 'count' in dependencies
useEffect(() => {
console.log(count);
}, []);
// โ
Correct
useEffect(() => {
console.log(count);
}, [count]);
2. Infinite Loops
// โ Wrong - creates infinite loop
useEffect(() => {
setCount(count + 1);
}, [count]); // count changes โ effect runs โ count changes...
// โ
Use different trigger or remove dependency
useEffect(() => {
// Run once on mount
setCount(c => c + 1);
}, []);
3. Async useEffect Callback
// โ Wrong - can't make callback async
useEffect(async () => {
const data = await fetchData();
}, []);
// โ
Correct - async function inside
useEffect(() => {
const fetchData = async () => {
const data = await fetch('/api');
};
fetchData();
}, []);
๐ฏ Key Takeaways
- Side effects: Operations outside component (API, timers, DOM)
- Dependencies: Include all used props/state/functions
- Empty array []: Run once on mount
- No array: Run after every render (rarely needed)
- Cleanup: Return function to clean up subscriptions/timers
- Async: Can't make callback async, use async function inside
- Race conditions: Use cancellation flags or AbortController
- Don't fetch in render: Always use useEffect for data fetching