Unlock the full potential of React's useEffect hook for robust side effect management. This guide covers fundamental concepts, common patterns, advanced techniques, and crucial best practices for global developers.
Mastering React useEffect: A Comprehensive Guide to Side Effect Management Patterns
In the dynamic world of modern web development, React stands out as a powerful library for building user interfaces. Its component-based architecture encourages declarative programming, making UI creation intuitive and efficient. However, applications rarely exist in isolation; they often need to interact with the outside world – fetching data, setting up subscriptions, manipulating the DOM, or integrating with third-party libraries. These interactions are known as "side effects".
Enter the useEffect hook, a cornerstone of functional components in React. Introduced with React Hooks, useEffect provides a powerful and elegant way to manage these side effects, bringing the capabilities previously found in class component lifecycle methods (like componentDidMount, componentDidUpdate, and componentWillUnmount) directly into functional components. Understanding and mastering useEffect is not just about writing cleaner code; it's about building more performant, reliable, and maintainable React applications.
This comprehensive guide will take you on a deep dive into useEffect, exploring its fundamental principles, common use cases, advanced patterns, and crucial best practices. Whether you're a seasoned React developer looking to solidify your understanding or new to hooks and eager to grasp this essential concept, you'll find valuable insights here. We'll cover everything from basic data fetching to complex dependency management, ensuring you're equipped to handle any side effect scenario.
1. Understanding the Fundamentals of useEffect
At its core, useEffect allows you to perform side effects in functional components. It essentially tells React that your component needs to do something after render. React will then run your "effect" function after it has flushed changes to the DOM.
What are Side Effects in React?
Side effects are operations that affect the outside world or interact with an external system. In the context of React, this often means:
- Data Fetching: Making API calls to retrieve or send data.
- Subscriptions: Setting up event listeners (e.g., for user input, global events), WebSocket connections, or real-time data streams.
- DOM Manipulation: Directly interacting with the browser's Document Object Model (e.g., changing the document title, managing focus, integrating with non-React libraries).
- Timers: Using
setTimeoutorsetInterval. - Logging: Sending analytics data.
Basic useEffect Syntax
The useEffect hook takes two arguments:
- A function that contains the side effect logic. This function can optionally return a cleanup function.
- An optional dependency array.
import React, { useEffect, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// This is the side effect function
console.log('Component rendered or count changed:', count);
// Optional cleanup function
return () => {
console.log('Cleanup for count:', count);
};
}, [count]); // Dependency array
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
The Dependency Array: The Key to Control
The second argument to useEffect, the dependency array, is crucial for controlling when the effect runs. React will re-run the effect only if any of the values in the dependency array have changed between renders.
-
No dependency array: The effect runs after every render of the component. This is rarely what you want for performance-critical effects like data fetching, as it can lead to infinite loops or unnecessary re-executions.
useEffect(() => { // Runs after every render }); -
Empty dependency array (
[]): The effect runs only once after the initial render (mount) and the cleanup function runs only once before the component unmounts. This is ideal for effects that should only happen once, like initial data fetching or setting up global event listeners.useEffect(() => { // Runs once on mount console.log('Component mounted!'); return () => { // Runs once on unmount console.log('Component unmounted!'); }; }, []); -
Dependency array with values (
[propA, stateB]): The effect runs after the initial render and whenever any of the values in the array change. This is the most common and versatile use case, ensuring your effect logic is synchronized with relevant data changes.useEffect(() => { // Runs on mount and whenever 'userId' changes fetchUser(userId); }, [userId]);
The Cleanup Function: Preventing Leaks and Bugs
Many side effects require a "cleanup" step. For instance, if you set up a subscription, you need to unsubscribe when the component unmounts to prevent memory leaks. If you start a timer, you need to clear it. The cleanup function is returned from your useEffect callback.
React runs the cleanup function before re-running the effect (if dependencies change) and before the component unmounts. This ensures that resources are properly released and potential issues like race conditions or stale closures are mitigated.
useEffect(() => {
const subscription = subscribeToChat(props.chatId);
return () => {
// Cleanup: Unsubscribe when chatId changes or component unmounts
unsubscribeFromChat(subscription);
};
}, [props.chatId]);
2. Common useEffect Use Cases and Patterns
Let's explore practical scenarios where useEffect shines, along with best practices for each.
2.1. Data Fetching
Data fetching is perhaps the most common use case for useEffect. You want to fetch data when the component mounts or when specific props/state values change.
Basic Fetch on Mount
import React, { useEffect, useState } from 'react';
function UserProfile() {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUserData = async () => {
try {
const response = await fetch('https://api.example.com/users/1');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUserData(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchUserData();
}, []); // Empty array ensures this runs only once on mount
if (loading) return <p>Loading user data...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!userData) return <p>No user data found.</p>;
return (
<div>
<h2>{userData.name}</h2>
<p>Email: {userData.email}</p>
<p>Location: {userData.location}</p>
</div>
);
}
Fetching with Dependencies
Often, the data you fetch depends on some dynamic value, like a user ID, a search query, or a page number. When these dependencies change, you want to re-fetch the data.
import React, { useEffect, useState } from 'react';
function UserPosts({ userId }) {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!userId) { // Handle cases where userId might be undefined initially
setPosts([]);
setLoading(false);
return;
}
const fetchUserPosts = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`https://api.example.com/users/${userId}/posts`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setPosts(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchUserPosts();
}, [userId]); // Re-fetch whenever userId changes
if (loading) return <p>Loading posts...</p>;
if (error) return <p>Error: {error.message}</p>;
if (posts.length === 0) return <p>No posts found for this user.</p>;
return (
<div>
<h3>Posts by User {userId}</h3>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Handling Race Conditions with Data Fetching
When dependencies change rapidly, you might encounter race conditions where an older, slower network request completes after a newer, faster one, leading to outdated data being displayed. A common pattern to mitigate this is to use a flag or an AbortController.
import React, { useEffect, useState } from 'react';
function ProductDetails({ productId }) {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
const fetchProduct = async () => {
setLoading(true);
setError(null);
setProduct(null); // Clear previous product data
try {
const response = await fetch(`https://api.example.com/products/${productId}`, { signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setProduct(data);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(err);
}
} finally {
setLoading(false);
}
};
fetchProduct();
return () => {
// Abort ongoing fetch request if component unmounts or productId changes
controller.abort();
};
}, [productId]);
if (loading) return <p>Loading product details...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!product) return <p>No product found.</p>;
return (
<div>
<h2>{product.name}</h2>
<p>Price: ${product.price}</p>
<p>Description: {product.description}</p>
</div>
);
}
2.2. Event Listeners and Subscriptions
Managing event listeners (e.g., keyboard events, window resize) or external subscriptions (e.g., WebSockets, chat services) is a classic side effect. The cleanup function is vital here to prevent memory leaks and ensure event handlers are removed when no longer needed.
Global Event Listener
import React, { useEffect, useState } from 'react';
function WindowSizeLogger() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => {
// Clean up the event listener when component unmounts
window.removeEventListener('resize', handleResize);
};
}, []); // Empty array: add/remove listener only once on mount/unmount
return (
<div>
<p>Window Width: {windowSize.width}px</p>
<p>Window Height: {windowSize.height}px</p>
</div>
);
}
Chat Service Subscription
import React, { useEffect, useState } from 'react';
// Assume chatService is an external module providing subscribe/unsubscribe methods
import { chatService } from './chatService';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const handleNewMessage = (message) => {
setMessages((prevMessages) => [...prevMessages, message]);
};
const subscription = chatService.subscribe(roomId, handleNewMessage);
return () => {
chatService.unsubscribe(subscription);
};
}, [roomId]); // Re-subscribe if roomId changes
return (
<div>
<h3>Chat Room: {roomId}</h3>
<div className="messages">
{messages.length === 0 ? (
<p>No messages yet.</p>
) : (
messages.map((msg, index) => (
<p key={index}><strong>{msg.sender}:</strong> {msg.text}</p>
))
)}
</div>
</div>
);
}
2.3. DOM Manipulation
While React's declarative nature often abstracts away direct DOM manipulation, there are times when you need to interact with the raw DOM, especially when integrating with third-party libraries that expect direct DOM access.
Modifying Document Title
import React, { useEffect } from 'react';
function PageTitleUpdater({ title }) {
useEffect(() => {
document.title = `My App | ${title}`;
}, [title]); // Update title whenever 'title' prop changes
return (
<h2>Welcome to the {title} Page!</h2>
);
}
Integrating with a Third-Party Chart Library (e.g., Chart.js)
import React, { useEffect, useRef } from 'react';
import Chart from 'chart.js/auto'; // Assuming Chart.js is installed
function MyChartComponent({ data, labels }) {
const chartRef = useRef(null); // Ref to hold the canvas element
const chartInstance = useRef(null); // Ref to hold the chart instance
useEffect(() => {
if (chartRef.current) {
// Destroy existing chart instance before creating a new one
if (chartInstance.current) {
chartInstance.current.destroy();
}
const ctx = chartRef.current.getContext('2d');
chartInstance.current = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Sales Data',
data: data,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
}
});
}
return () => {
// Cleanup: Destroy the chart instance on unmount
if (chartInstance.current) {
chartInstance.current.destroy();
}
};
}, [data, labels]); // Re-render chart if data or labels change
return (
<div style={{ width: '600px', height: '400px' }}>
<canvas ref={chartRef}></canvas>
</div>
);
}
2.4. Timers
Using setTimeout or setInterval within React components requires careful management to prevent timers from continuing to run after a component has unmounted, which can lead to errors or memory leaks.
Simple Countdown Timer
import React, { useEffect, useState } from 'react';
function CountdownTimer({ initialSeconds }) {
const [seconds, setSeconds] = useState(initialSeconds);
useEffect(() => {
if (seconds <= 0) return; // Stop timer when it reaches zero
const timerId = setInterval(() => {
setSeconds(prevSeconds => prevSeconds - 1);
}, 1000);
return () => {
// Cleanup: Clear the interval when component unmounts or seconds become 0
clearInterval(timerId);
};
}, [seconds]); // Re-run effect if seconds changes to set up new interval (e.g. if initialSeconds changes)
return (
<div>
<h3>Countdown: {seconds} seconds</h3>
{seconds === 0 && <p>Time's up!</p>}
</div>
);
}
3. Advanced useEffect Patterns and Pitfalls
While the basics of useEffect are straightforward, mastering it involves understanding more subtle behaviors and common pitfalls.
3.1. Stale Closures and Outdated Values
A common issue with `useEffect` (and JavaScript closures in general) is accessing "stale" values from a previous render. If your effect closure captures a state or prop that changes, but you don't include it in the dependency array, the effect will continue to see the old value.
Consider this problematic example:
import React, { useEffect, useState } from 'react';
function StaleClosureExample() {
const [count, setCount] = useState(0);
useEffect(() => {
// This effect wants to log the count after 2 seconds.
// If count changes within these 2 seconds, this will log the OLD count!
const timer = setTimeout(() => {
console.log('Stale Count:', count);
}, 2000);
return () => {
clearTimeout(timer);
};
}, []); // Problem: 'count' is not in dependencies, so it's stale
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
To fix this, ensure all values used inside your effect that come from props or state are included in the dependency array:
import React, { useEffect, useState } from 'react';
function FixedClosureExample() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log('Correct Count:', count);
}, 2000);
return () => {
clearTimeout(timer);
};
}, [count]); // Solution: 'count' is now a dependency. Effect re-runs when count changes.
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
However, adding dependencies can sometimes lead to an effect running too often. This brings us to other patterns:
Using Functional Updates for State
When updating state based on its previous value, use the functional update form of set- functions. This eliminates the need to include the state variable in the dependency array.
import React, { useEffect, useState } from 'react';
function AutoIncrementer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1); // Functional update
}, 1000);
return () => clearInterval(interval);
}, []); // 'count' is not a dependency because we use functional update
return <p>Count: {count}</p>;
}
useRef for Mutable Values Not Causing Rerenders
Sometimes you need to store a mutable value that doesn't trigger re-renders, but is accessible inside your effect. useRef is perfect for this.
import React, { useEffect, useRef, useState } from 'react';
function LatestValueLogger() {
const [count, setCount] = useState(0);
const latestCountRef = useRef(count); // Create a ref
// Keep the ref's current value updated with the latest count
useEffect(() => {
latestCountRef.current = count;
}, [count]);
useEffect(() => {
const interval = setInterval(() => {
// Access the latest count via the ref, avoiding stale closure
console.log('Latest Count:', latestCountRef.current);
}, 2000);
return () => clearInterval(interval);
}, []); // Empty dependency array, as we are not directly using 'count' here
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useCallback and useMemo for Stable Dependencies
When a function or object is a dependency of your useEffect, it can cause the effect to re-run unnecessarily if the function/object reference changes on every render (which it typically does). useCallback and useMemo help by memoizing these values, providing a stable reference.
Problematic example:
import React, { useEffect, useState } from 'react';
function UserSettings() {
const [userId, setUserId] = useState(1);
const [settings, setSettings] = useState({});
const fetchSettings = async () => {
// This function is re-created on every render
console.log('Fetching settings for user:', userId);
const response = await fetch(`https://api.example.com/users/${userId}/settings`);
const data = await response.json();
setSettings(data);
};
useEffect(() => {
fetchSettings();
}, [fetchSettings]); // Problem: fetchSettings changes on every render
return (
<div>
<p>User ID: {userId}</p>
<button onClick={() => setUserId(userId + 1)}>Next User</button>
<pre>{JSON.stringify(settings, null, 2)}</pre>
</div>
);
}
Solution with useCallback:
import React, { useEffect, useState, useCallback } from 'react';
function UserSettingsOptimized() {
const [userId, setUserId] = useState(1);
const [settings, setSettings] = useState({});
const fetchSettings = useCallback(async () => {
console.log('Fetching settings for user:', userId);
const response = await fetch(`https://api.example.com/users/${userId}/settings`);
const data = await response.json();
setSettings(data);
}, [userId]); // fetchSettings only changes when userId changes
useEffect(() => {
fetchSettings();
}, [fetchSettings]); // Now fetchSettings is a stable dependency
return (
<div>
<p>User ID: {userId}</p>
<button onClick={() => setUserId(userId + 1)}>Next User</button>
<pre>{JSON.stringify(settings, null, 2)}</pre>
</div>
);
}
Similarly, for objects or arrays, use useMemo to create a stable reference:
import React, { useEffect, useMemo, useState } from 'react';
function ProductList({ categoryId, sortBy }) {
const [products, setProducts] = useState([]);
// Memoize the filter/sort criteria object
const fetchCriteria = useMemo(() => ({
category: categoryId,
sort: sortBy,
}), [categoryId, sortBy]);
useEffect(() => {
// fetch products based on fetchCriteria
console.log('Fetching products with criteria:', fetchCriteria);
// ... API call logic ...
}, [fetchCriteria]); // Effect runs only when categoryId or sortBy changes
return (
<div>
<h3>Products in Category {categoryId} (Sorted by {sortBy})</h3>
<!-- Render product list -->
</div>
);
}
3.2. Infinite Loops
An infinite loop can occur if an effect updates a state variable that is also in its dependency array, and the update always causes a re-render that triggers the effect again. This is a common pitfall when not careful with dependencies.
import React, { useEffect, useState } from 'react';
function InfiniteLoopExample() {
const [data, setData] = useState([]);
useEffect(() => {
// This will cause an infinite loop!
// setData causes a re-render, which re-runs the effect, which calls setData again.
setData([1, 2, 3]);
}, [data]); // 'data' is a dependency, and we're always setting a new array reference
return <p>Data length: {data.length}</p>;
}
To fix this, ensure your effect only runs when genuinely needed or use functional updates. If you only want to set data once on mount, use an empty dependency array.
import React, { useEffect, useState } from 'react';
function CorrectDataSetup() {
const [data, setData] = useState([]);
useEffect(() => {
// This runs only once on mount
setData([1, 2, 3]);
}, []); // Empty array prevents re-runs
return <p>Data length: {data.length}</p>;
}
3.3. Performance Optimization with useEffect
Splitting Concerns into Multiple useEffect Hooks
Instead of cramming all side effects into one large useEffect, split them into multiple hooks. Each useEffect can then manage its own set of dependencies and cleanup logic. This makes code more readable, maintainable, and often prevents unnecessary re-runs of unrelated effects.
import React, { useEffect, useState } from 'react';
function UserDashboard({ userId }) {
const [profile, setProfile] = useState(null);
const [activityLog, setActivityLog] = useState([]);
// Effect for fetching user profile (depends only on userId)
useEffect(() => {
const fetchProfile = async () => {
// ... fetch profile data ...
console.log('Fetching profile for', userId);
const response = await fetch(`https://api.example.com/users/${userId}/profile`);
const data = await response.json();
setProfile(data);
};
fetchProfile();
}, [userId]);
// Effect for fetching activity log (also depends on userId, but separate concern)
useEffect(() => {
const fetchActivity = async () => {
// ... fetch activity data ...
console.log('Fetching activity for', userId);
const response = await fetch(`https://api.example.com/users/${userId}/activity`);
const data = await response.json();
setActivityLog(data);
};
fetchActivity();
}, [userId]);
return (
<div>
<h2>User Dashboard: {userId}</h2>
<h3>Profile:</h3>
<pre>{JSON.stringify(profile, null, 2)}</pre>
<h3>Activity Log:</h3>
<pre>{JSON.stringify(activityLog, null, 2)}</pre>
</div>
);
}
3.4. Custom Hooks for Reusability
When you find yourself writing the same useEffect logic across multiple components, it's a strong indicator that you can abstract it into a custom hook. Custom hooks are functions that start with use and can call other hooks, making your logic reusable and easier to test.
Example: useFetch Custom Hook
import React, { useEffect, useState } from 'react';
// Custom Hook: useFetch.js
function useFetch(url, dependencies = []) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(err);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => {
abortController.abort();
};
}, [url, ...dependencies]); // Re-run if URL or any extra dependency changes
return { data, loading, error };
}
// Component using the custom hook: UserDataDisplay.js
function UserDataDisplay({ userId }) {
const { data: userData, loading, error } = useFetch(
`https://api.example.com/users/${userId}`,
[userId] // Pass userId as a dependency to the custom hook
);
if (loading) return <p>Loading user data...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!userData) return <p>No user data.</p>;
return (
<div>
<h2>{userData.name}</h2>
<p>Email: {userData.email}</p>
</div>
);
}
```
4. When *Not* to Use useEffect
While powerful, useEffect is not always the right tool for every job. Misusing it can lead to unnecessary complexity, performance issues, or hard-to-debug logic.
4.1. For Derived State or Computed Values
If you have state that can be computed directly from other existing state or props, you don't need useEffect. Calculate it directly during render.
Bad Practice:
function ProductCalculator({ price, quantity }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(price * quantity); // Unnecessary effect
}, [price, quantity]);
return <p>Total: ${total.toFixed(2)}</p>;
}
Good Practice:
function ProductCalculator({ price, quantity }) {
const total = price * quantity; // Computed directly
return <p>Total: ${total.toFixed(2)}</p>;
}
If the computation is expensive, consider useMemo, but still not useEffect.
import React, { useMemo } from 'react';
function ComplexProductCalculator({ items }) {
const memoizedTotal = useMemo(() => {
console.log('Recalculating total...');
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}, [items]);
return <p>Complex Total: ${memoizedTotal.toFixed(2)}</p>;
}
4.2. For Prop or State Changes That Should Trigger a Re-render of Child Components
The primary way to pass data down to children and trigger their re-renders is through props. Don't use useEffect in a parent component to update state that then gets passed as a prop, when a direct prop update would suffice.
4.3. For Effects That Don't Require Cleanup and Are Purely Visual
If your side effect is purely visual and doesn't involve any external systems, subscriptions, or timers, and doesn't require cleanup, you might not need useEffect. For simple visual updates or animations that don't depend on external state, CSS or direct React component rendering might be sufficient.
Conclusion: Mastering useEffect for Robust Applications
The useEffect hook is an indispensable part of building robust and reactive React applications. It elegantly bridges the gap between React's declarative UI and the imperative nature of side effects. By understanding its fundamental principles – the effect function, the dependency array, and the crucial cleanup mechanism – you gain fine-grained control over when and how your side effects execute.
We've explored a wide array of patterns, from common data fetching and event management to handling complex scenarios like race conditions and stale closures. We've also highlighted the power of custom hooks in abstracting and reusing effect logic, a practice that significantly enhances code maintainability and readability across diverse projects and global teams.
Remember these key takeaways to master useEffect:
- Identify True Side Effects: Use
useEffectfor interactions with the "outside world" (APIs, DOM, subscriptions, timers). - Manage Dependencies Meticulously: The dependency array is your primary control. Be explicit about what values your effect relies on to prevent stale closures and unnecessary re-runs.
- Prioritize Cleanup: Always consider if your effect requires cleanup (e.g., unsubscribing, clearing timers, aborting requests) to prevent memory leaks and ensure application stability.
- Separate Concerns: Use multiple
useEffecthooks for distinct, unrelated side effects within a single component. - Leverage Custom Hooks: Encapsulate complex or reusable
useEffectlogic into custom hooks to improve modularity and reusability. - Avoid Common Pitfalls: Be wary of infinite loops and ensure you're not using
useEffectfor simple derived state or direct prop passing.
By applying these patterns and best practices, you'll be well-equipped to manage side effects in your React applications with confidence, building high-quality, performant, and scalable user experiences for users across the globe. Keep experimenting, keep learning, and keep building amazing things with React!