Component Lifecycle and Effects
1. useEffect Hook Patterns and Dependencies
| Pattern | Dependencies | Runs When | Use Case |
|---|---|---|---|
| No dependencies | useEffect(() => {}) |
Every render | Rarely needed, usually a mistake |
| Empty array | useEffect(() => {}, []) |
Once on mount | Initial setup, subscriptions |
| With dependencies | useEffect(() => {}, [a, b]) |
When a or b changes | Sync with specific values |
| Single dependency | useEffect(() => {}, [id]) |
When id changes | Fetch data for specific ID |
| Object dependency | useEffect(() => {}, [obj]) |
When object reference changes | Can cause extra renders |
| Function dependency | useEffect(() => {}, [fn]) |
When function reference changes | Needs useCallback wrapper |
| Dependency Rule | Description | ESLint Rule |
|---|---|---|
| Include all used values | List all props, state, vars used in effect | exhaustive-deps |
| Primitive values | Numbers, strings, booleans - safe | No warning |
| Object/array deps | New reference = triggers effect | May need useMemo |
| Function deps | Wrap with useCallback | May need useCallback |
| Refs don't trigger | ref.current changes ignored |
Safe to omit |
| setState stable | setState functions don't change | Safe to omit |
Example: useEffect dependency patterns
// Run once on mount (componentDidMount equivalent)
useEffect(() => {
console.log('Component mounted');
document.title = 'My App';
}, []); // Empty dependency array
// Run on every render (usually wrong!)
useEffect(() => {
console.log('Every render'); // Performance issue!
}); // No dependency array - avoid this
// Run when specific values change
useEffect(() => {
console.log(\`Count changed to \${count}\`);
}, [count]); // Re-runs when count changes
// Multiple dependencies
useEffect(() => {
console.log(\`User \${userId} on page \${page}\`);
fetchUserData(userId, page);
}, [userId, page]); // Re-runs when either changes
// Derived values in effect
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchUser = async () => {
const data = await fetch(\`/api/users/\${userId}\`).then(r => r.json());
if (!cancelled) {
setUser(data);
}
};
fetchUser();
return () => {
cancelled = true;
};
}, [userId]); // Only userId dependency needed
return user ? <div>{user.name}</div> : <div>Loading...</div>;
};
// Object dependency issue
const BadObjectDep = ({ config }) => {
useEffect(() => {
// This runs every render if config is recreated!
console.log(config.apiUrl);
}, [config]); // ❌ Object reference changes each render
return null;
};
// Fix: Extract primitive values
const GoodObjectDep = ({ config }) => {
useEffect(() => {
console.log(config.apiUrl);
}, [config.apiUrl]); // ✅ Only re-run if apiUrl changes
return null;
};
// Function dependency issue
const Parent = () => {
const [count, setCount] = useState(0);
// ❌ New function every render
const handleClick = () => {
console.log(count);
};
return <Child onClick={handleClick} />;
};
const Child = ({ onClick }) => {
useEffect(() => {
// Runs every render because onClick changes
console.log('onClick changed');
}, [onClick]); // ❌ Function reference changes
return <button onClick={onClick}>Click</button>;
};
// Fix: Use useCallback
const ParentFixed = () => {
const [count, setCount] = useState(0);
// ✅ Stable function reference
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
return <ChildFixed onClick={handleClick} />;
};
// setState doesn't need to be in dependencies
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // ✅ Functional update
}, 1000);
return () => clearInterval(timer);
}, []); // setCount not needed in deps
return <div>{count}</div>;
};
Warning: React's ESLint plugin (
eslint-plugin-react-hooks) will warn about
missing dependencies. Don't ignore these warnings - they prevent bugs. If you intentionally want to omit a
dependency, use // eslint-disable-next-line react-hooks/exhaustive-deps with a comment explaining
why.
2. Effect Cleanup and Memory Leak Prevention
| Cleanup Scenario | Without Cleanup | With Cleanup | Memory Leak Risk |
|---|---|---|---|
| Timers | Continues running after unmount | clearTimeout/Interval |
High |
| Event listeners | Accumulate on every mount | removeEventListener |
High |
| Subscriptions | Connection stays open | unsubscribe() |
High |
| Async operations | setState on unmounted component | Cancellation flag/AbortController | Medium |
| WebSocket | Connection remains open | socket.close() |
High |
| Animation frames | Continues running | cancelAnimationFrame |
Medium |
| Observers | Observers keep watching | observer.disconnect() |
High |
Example: Effect cleanup patterns
// Timer cleanup
const Timer = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup function
return () => {
clearInterval(interval);
console.log('Timer cleaned up');
};
}, []);
return <div>Seconds: {seconds}</div>;
};
// Event listener cleanup
const WindowSize = () => {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// Add listener
window.addEventListener('resize', handleResize);
handleResize(); // Initial call
// Cleanup: remove listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <div>{size.width} x {size.height}</div>;
};
// Async operation cleanup (prevent setState on unmounted)
const UserData = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false; // Cancellation flag
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(\`/api/users/\${userId}\`);
const data = await response.json();
// Only update state if not cancelled
if (!cancelled) {
setUser(data);
setLoading(false);
}
} catch (error) {
if (!cancelled) {
console.error(error);
setLoading(false);
}
}
};
fetchUser();
// Cleanup: set flag to prevent setState
return () => {
cancelled = true;
};
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
};
// AbortController for fetch cleanup (modern approach)
const DataFetcher = ({ endpoint }) => {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(endpoint, {
signal: controller.signal
});
const result = await response.json();
setData(result);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error(error);
}
}
};
fetchData();
// Cleanup: abort fetch
return () => {
controller.abort();
};
}, [endpoint]);
return <div>{JSON.stringify(data)}</div>;
};
// WebSocket cleanup
const LiveChat = ({ roomId }) => {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket(\`wss://chat.example.com/\${roomId}\`);
ws.onopen = () => {
console.log('Connected to chat');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Cleanup: close WebSocket
return () => {
ws.close();
console.log('Disconnected from chat');
};
}, [roomId]);
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.text}</div>
))}
</div>
);
};
// IntersectionObserver cleanup
const LazyImage = ({ src, alt }) => {
const [isVisible, setIsVisible] = useState(false);
const imgRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect(); // Stop observing once visible
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
// Cleanup: disconnect observer
return () => {
observer.disconnect();
};
}, []);
return (
<div ref={imgRef}>
{isVisible ? (
<img src={src} alt={alt} />
) : (
<div>Loading...</div>
)}
</div>
);
};
// Animation frame cleanup
const AnimatedCounter = ({ target }) => {
const [count, setCount] = useState(0);
useEffect(() => {
let frame;
let startTime = null;
const animate = (timestamp) => {
if (!startTime) startTime = timestamp;
const progress = timestamp - startTime;
const nextCount = Math.min(
Math.floor((progress / 2000) * target),
target
);
setCount(nextCount);
if (nextCount < target) {
frame = requestAnimationFrame(animate);
}
};
frame = requestAnimationFrame(animate);
// Cleanup: cancel animation
return () => {
cancelAnimationFrame(frame);
};
}, [target]);
return <div>{count}</div>;
};
Warning: Always clean up side effects! Common memory leaks: forgetting to clear timers,
not removing event listeners, leaving WebSockets open, or calling setState on unmounted components.
3. Effect Timing and useLayoutEffect
| Hook | Timing | Blocks Paint | Use Case |
|---|---|---|---|
| useEffect | After paint (async) | No | Most side effects, data fetching |
| useLayoutEffect | Before paint (sync) | Yes | DOM measurements, prevent flicker |
| Execution Order | Phase | Description |
|---|---|---|
| 1. Render | Component function runs | JSX returned, state/props evaluated |
| 2. React updates DOM | DOM mutations | Changes applied to DOM |
| 3. useLayoutEffect | Before browser paint | Synchronous, blocks painting |
| 4. Browser paints | Visual update | User sees changes |
| 5. useEffect | After paint | Async, doesn't block paint |
Example: useEffect vs useLayoutEffect timing
// useEffect - runs AFTER paint (preferred)
const DataComponent = ({ id }) => {
const [data, setData] = useState(null);
useEffect(() => {
// Runs after component is painted
// Good for data fetching - doesn't block UI
fetchData(id).then(setData);
}, [id]);
return <div>{data?.name}</div>;
};
// useLayoutEffect - runs BEFORE paint
const TooltipPosition = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const tooltipRef = useRef(null);
useLayoutEffect(() => {
// Measure DOM before browser paints
if (tooltipRef.current) {
const rect = tooltipRef.current.getBoundingClientRect();
// Adjust position if off screen
let x = rect.left;
let y = rect.top;
if (x + rect.width > window.innerWidth) {
x = window.innerWidth - rect.width - 10;
}
if (y + rect.height > window.innerHeight) {
y = window.innerHeight - rect.height - 10;
}
setPosition({ x, y });
}
}); // Runs on every render
return (
<div
ref={tooltipRef}
style={{ left: position.x, top: position.y }}
>
Tooltip
</div>
);
};
// Prevent visual flicker with useLayoutEffect
const AnimatedHeight = ({ isOpen, children }) => {
const [height, setHeight] = useState(0);
const contentRef = useRef(null);
useLayoutEffect(() => {
if (isOpen && contentRef.current) {
// Measure and set height BEFORE paint
// Prevents flicker from 0 to auto height
const contentHeight = contentRef.current.scrollHeight;
setHeight(contentHeight);
} else {
setHeight(0);
}
}, [isOpen]);
return (
<div
style={{
height: height,
overflow: 'hidden',
transition: 'height 0.3s ease'
}}
>
<div ref={contentRef}>{children}</div>
</div>
);
};
// DOM mutation before paint
const ScrollToTop = ({ trigger }) => {
useLayoutEffect(() => {
// Scroll happens before user sees the page
window.scrollTo(0, 0);
}, [trigger]);
return null;
};
// Comparison: flicker with useEffect
const FlickerExample = () => {
const [width, setWidth] = useState(0);
const ref = useRef(null);
// ❌ WRONG: causes visible flicker
useEffect(() => {
// Runs AFTER paint - user sees wrong width first
if (ref.current) {
setWidth(ref.current.offsetWidth);
}
}, []);
return <div ref={ref}>Width: {width}px</div>;
};
// Fixed: no flicker with useLayoutEffect
const NoFlickerExample = () => {
const [width, setWidth] = useState(0);
const ref = useRef(null);
// ✅ CORRECT: measures before paint
useLayoutEffect(() => {
// Runs BEFORE paint - correct width from start
if (ref.current) {
setWidth(ref.current.offsetWidth);
}
}, []);
return <div ref={ref}>Width: {width}px</div>;
};
// Focus management with useLayoutEffect
const AutoFocus = ({ shouldFocus }) => {
const inputRef = useRef(null);
useLayoutEffect(() => {
if (shouldFocus && inputRef.current) {
// Focus before paint - no visual jump
inputRef.current.focus();
}
}, [shouldFocus]);
return <input ref={inputRef} />;
};
Rule of thumb: Use
useEffect for 99% of cases. Only use
useLayoutEffect
when you need to read layout (measurements, scroll position) or prevent visual flicker. useLayoutEffect can hurt
performance because it blocks painting.
4. Data Fetching with useEffect Patterns
| Pattern | Best Practice | Issues to Handle |
|---|---|---|
| Basic fetch | async/await in effect | Loading state, errors |
| Cleanup | Cancellation flag or AbortController | setState on unmounted component |
| Dependencies | Include all params used in fetch | Stale data, race conditions |
| Race conditions | Ignore results from outdated requests | Wrong data displayed |
| Error handling | Try/catch and error state | Unhandled rejections |
| Loading state | Boolean flag while fetching | Poor UX without feedback |
Example: Data fetching patterns
// Basic data fetching
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(\`/api/users/\${userId}\`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [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>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
// With cleanup to prevent setState on unmounted
const DataFetchWithCleanup = ({ endpoint }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
const response = await fetch(endpoint);
const result = await response.json();
if (!cancelled) {
setData(result);
setLoading(false);
}
} catch (error) {
if (!cancelled) {
console.error(error);
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [endpoint]);
return loading ? <div>Loading...</div> : <div>{JSON.stringify(data)}</div>;
};
// Race condition handling
const SearchResults = ({ query }) => {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
let cancelled = false;
setLoading(true);
const search = async () => {
try {
const response = await fetch(\`/api/search?q=\${query}\`);
const data = await response.json();
// Only update if this is still the latest query
if (!cancelled) {
setResults(data);
setLoading(false);
}
} catch (error) {
if (!cancelled) {
console.error(error);
setLoading(false);
}
}
};
search();
// Cleanup: ignore results if query changes
return () => {
cancelled = true;
};
}, [query]);
return (
<div>
{loading && <div>Searching...</div>}
<ul>
{results.map(r => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
};
// AbortController pattern (modern)
const ModernFetch = ({ url }) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(url, {
signal: controller.signal
});
const result = await response.json();
setData(result);
} catch (err) {
// Don't set error if aborted
if (err.name !== 'AbortError') {
setError(err.message);
}
}
};
fetchData();
return () => {
controller.abort();
};
}, [url]);
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
};
// Debounced search
const DebouncedSearch = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
setLoading(true);
// Debounce: wait 500ms after user stops typing
const timer = setTimeout(async () => {
try {
const response = await fetch(\`/api/search?q=\${query}\`);
const data = await response.json();
setResults(data);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}, 500);
// Cleanup: cancel previous timeout
return () => {
clearTimeout(timer);
};
}, [query]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
{loading && <div>Searching...</div>}
<ul>
{results.map(r => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
};
// Multiple parallel requests
const Dashboard = () => {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [notifications, setNotifications] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchAll = async () => {
try {
// Fetch all in parallel
const [userRes, postsRes, notifRes] = await Promise.all([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/notifications')
]);
const [userData, postsData, notifData] = await Promise.all([
userRes.json(),
postsRes.json(),
notifRes.json()
]);
setUser(userData);
setPosts(postsData);
setNotifications(notifData);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
fetchAll();
}, []);
if (loading) return <div>Loading dashboard...</div>;
return (
<div>
<h1>Welcome {user?.name}</h1>
<div>Posts: {posts.length}</div>
<div>Notifications: {notifications.length}</div>
</div>
);
};
Warning: For production apps, consider using dedicated data fetching libraries like
SWR or React Query instead of manual useEffect. They handle caching, revalidation,
race conditions, and optimistic updates automatically.
5. Custom Hooks for Effect Logic
| Custom Hook | Encapsulates | Returns | Reusability |
|---|---|---|---|
| useFetch | Data fetching logic | {data, loading, error} | High - any API endpoint |
| useDebounce | Debounced value updates | Debounced value | High - search, filters |
| useInterval | Timer with cleanup | void | Medium - polling |
| useEventListener | Event subscription | void | High - any event |
| useLocalStorage | localStorage sync | [value, setValue] | High - persistence |
| useWindowSize | Window dimensions | {width, height} | High - responsive |
Example: Reusable custom hooks
// useFetch - reusable data fetching
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) return;
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(\`HTTP error! status: \${response.status}\`);
}
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
};
// Usage
const UserProfile = ({ userId }) => {
const { data: user, loading, error } = useFetch(\`/api/users/\${userId}\`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{user.name}</div>;
};
// useDebounce - delay value updates
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
};
// Usage
const SearchComponent = () => {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 500);
const { data } = useFetch(\`/api/search?q=\${debouncedSearch}\`);
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<div>{JSON.stringify(data)}</div>
</div>
);
};
// useInterval - declarative setInterval
const useInterval = (callback, delay) => {
const savedCallback = useRef();
// Remember latest callback
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up interval
useEffect(() => {
if (delay === null) return;
const tick = () => {
savedCallback.current();
};
const id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
};
// Usage
const Clock = () => {
const [time, setTime] = useState(new Date());
useInterval(() => {
setTime(new Date());
}, 1000);
return <div>{time.toLocaleTimeString()}</div>;
};
// useEventListener - declarative addEventListener
const useEventListener = (eventName, handler, element = window) => {
const savedHandler = useRef();
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const isSupported = element && element.addEventListener;
if (!isSupported) return;
const eventListener = (event) => savedHandler.current(event);
element.addEventListener(eventName, eventListener);
return () => {
element.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
};
// Usage
const KeyLogger = () => {
const [key, setKey] = useState('');
useEventListener('keydown', (e) => {
setKey(e.key);
});
return <div>Last key: {key}</div>;
};
// useLocalStorage - sync state with localStorage
const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function
? value(storedValue)
: value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
};
// Usage
const ThemeToggle = () => {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
};
// useWindowSize - track window dimensions
const useWindowSize = () => {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
};
// Usage
const ResponsiveComponent = () => {
const { width } = useWindowSize();
return (
<div>
{width < 768 ? 'Mobile' : 'Desktop'} - {width}px
</div>
);
};
Note: Custom hooks let you extract and reuse effect logic across components. Name them with
use prefix and follow all hooks rules. They can use other hooks internally.
6. Effect Performance and Optimization
| Optimization | Technique | Impact | When to Use |
|---|---|---|---|
| Proper dependencies | Only include what changes | Avoid unnecessary runs | Always |
| useMemo deps | Memoize object/array deps | Prevent extra effect runs | Object dependencies |
| useCallback deps | Memoize function deps | Stable function reference | Function dependencies |
| Split effects | One effect per concern | Run only what's needed | Multiple responsibilities |
| Functional updates | setState(prev => ...) |
Remove state from deps | State-dependent updates |
| Debouncing | Delay effect execution | Reduce frequency | Frequent changes (input) |
| Throttling | Limit execution rate | Control frequency | Continuous events (scroll) |
Example: Effect performance optimizations
// ❌ BAD: Unnecessary effect runs
const BadDeps = ({ user }) => {
useEffect(() => {
console.log(user.name);
}, [user]); // Runs every time user object changes (even if name is same)
return null;
};
// ✅ GOOD: Only depend on what you use
const GoodDeps = ({ user }) => {
useEffect(() => {
console.log(user.name);
}, [user.name]); // Only runs when name actually changes
return null;
};
// ❌ BAD: Object dependency causes extra runs
const ParentBad = () => {
const [count, setCount] = useState(0);
// New object every render!
const config = { apiUrl: 'https://api.example.com' };
return <ChildBad config={config} />;
};
const ChildBad = ({ config }) => {
useEffect(() => {
fetch(config.apiUrl); // Runs every render!
}, [config]);
return null;
};
// ✅ GOOD: Memoize object dependency
const ParentGood = () => {
const [count, setCount] = useState(0);
// Same object reference across renders
const config = useMemo(
() => ({ apiUrl: 'https://api.example.com' }),
[]
);
return <ChildGood config={config} />;
};
const ChildGood = ({ config }) => {
useEffect(() => {
fetch(config.apiUrl); // Only runs once
}, [config]);
return null;
};
// ❌ BAD: Multiple concerns in one effect
const BadMultiEffect = ({ userId, theme }) => {
useEffect(() => {
// Fetch user data
fetchUser(userId).then(setUser);
// Apply theme
document.body.className = theme;
// Both run on every userId OR theme change!
}, [userId, theme]);
return null;
};
// ✅ GOOD: Split into separate effects
const GoodMultiEffect = ({ userId, theme }) => {
// Effect 1: User data (only re-runs when userId changes)
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// Effect 2: Theme (only re-runs when theme changes)
useEffect(() => {
document.body.className = theme;
}, [theme]);
return null;
};
// ❌ BAD: State in dependencies
const BadStateDep = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // Uses stale count
}, 1000);
return () => clearInterval(timer);
}, [count]); // Re-creates interval on every count change!
return <div>{count}</div>;
};
// ✅ GOOD: Functional update removes dependency
const GoodStateDep = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // Functional update
}, 1000);
return () => clearInterval(timer);
}, []); // No dependencies - interval created once
return <div>{count}</div>;
};
// Debouncing to reduce effect frequency
const SearchWithDebounce = () => {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
// Wait for user to stop typing
const timer = setTimeout(() => {
if (searchTerm) {
fetch(\`/api/search?q=\${searchTerm}\`)
.then(r => r.json())
.then(setResults);
}
}, 500); // Debounce 500ms
return () => clearTimeout(timer);
}, [searchTerm]);
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
<ul>
{results.map(r => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
};
// Throttling for continuous events
const ScrollTracker = () => {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
let ticking = false;
const handleScroll = () => {
if (!ticking) {
window.requestAnimationFrame(() => {
setScrollY(window.scrollY);
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <div>Scroll position: {scrollY}px</div>;
};
// Conditional effect execution
const ConditionalEffect = ({ shouldFetch, userId }) => {
useEffect(() => {
// Early return to skip effect
if (!shouldFetch) return;
fetchUser(userId).then(setUser);
}, [shouldFetch, userId]);
return null;
};