When starting your journey with React, you'll focus on building user interfaces (UI) from components, state, and props. Everything seems very "pure": components receive input (props, state) and return output (JSX). But then one day, you wonder: "How do I fetch data from a server? How do I change the page title? Or how do I set up a timer?".
Welcome to Side Effects - a core concept that often confuses beginners. Don't worry! This article will decode everything about side effects in the most intuitive and easy-to-understand way.
π€ What are Side Effects? The Simplest Way to Understand
Imagine your React component as a professional chef π¨βπ³. His main job is to receive ingredients (props, state) and cook a perfect dish (render the UI). This process is called a "pure function" - a closed, predictable process.
Side effects are all the actions that the chef must do outside of his kitchen.
Examples:
- Going to the market to buy ingredients: Equivalent to calling an API to fetch data from a server.
- Setting a timer for the oven: Equivalent to using
setTimeout
orsetInterval
. - Writing down recipes in a notebook: Equivalent to logging or saving data to
localStorage
. - Changing the "Dish of the Day" sign: Equivalent to directly manipulating the DOM (e.g., changing
document.title
).
In summary, side effects in React are any actions that your component performs to interact with the world outside of its normal render flow. That outside world could be an API, Local Storage, the browser's DOM, or any other system not directly controlled by React.
π₯ Why Do We Need to "Manage" Side Effects?
Why can't we just put a fetch()
command directly inside the component body?
// βοΈ DON'T DO THIS!
function MyComponent() {
// Wrong! This fetch will run every time the component re-renders
fetch('https://api.example.com/data')
.then((res) => res.json())
.then((data) => console.log(data))
return <div>My Data</div>
}
The problem is that React can re-render a component multiple times for various reasons (state changes, props changes...). If you place side effects directly like above, they will run every time it renders. This leads to serious consequences:
- Infinite Loop: Call API, then update state, updating state causes re-render, re-render calls API again... and so on in a loop.
- Poor Performance: Sending hundreds of unnecessary network requests.
- Unpredictable Behavior: You can't control when side effects will occur.
Therefore, React needs a "safe zone," a special place where we can execute and manage these side effects in a controlled manner. And that hero is...
π¦ΈββοΈ useEffect - The Ultimate Weapon for Handling Side Effects
useEffect
is a Hook provided by React that allows you to perform side effects from within function components. It's like telling React: "Hey React, after you finish rendering the UI, please run this code for me!".
Basic Syntax
useEffect
takes two arguments:
- A function containing your side effect code.
- A dependency array (optional) to control when the effect runs again.
import { useEffect } from 'react'
useEffect(() => {
// Your side effect code will go here
console.log('Component has been rendered or updated!')
}, [dependencies]) // <-- Dependency array
useEffect
is a universal key, and its power lies in the dependency array.
Three Scenarios of the Dependency Array
-
No dependency array: Effect runs after every render.
useEffect(() => { // β οΈ Be careful: Runs after every component re-render. // Very easy to cause infinite loops. })
-
Empty array
[]
: Effect only runs once, right after the first render.useEffect(() => { // Perfect for calling API once or initial setup. fetchData() }, []) // <-- Empty array
-
Array with values
[prop, state]
: Effect will run first, and then only run again when one of the values in the array changes.const [userId, setUserId] = useState(1) useEffect(() => { // This effect will run again every time userId changes. fetchUser(userId) }, [userId]) // <-- Depends on userId
This is the most powerful and commonly used scenario.
π§Ή Cleanup Function
What happens if you set up a setInterval
or a WebSocket connection? If the component is removed from the DOM tree (unmount), those connections still exist and cause memory leaks.
useEffect
allows you to return a function from within it. This function is called a "cleanup function" and will be executed when:
- The component is about to unmount.
- Before the effect runs again on the next render.
useEffect(() => {
const timerId = setInterval(() => {
console.log('Tick!')
}, 1000)
// π This is the cleanup function
return () => {
console.log('Cleaning up timer...')
clearInterval(timerId) // Cancel timer when component unmounts
}
}, [])
π― Common useEffect Use Cases
Here are some real examples:
1. Fetching Data from API
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
async function fetchUserData() {
const response = await fetch(`https://api.example.com/users/${userId}`)
const data = await response.json()
setUser(data)
}
fetchUserData()
}, [userId]) // Run again when userId changes
return <div>{user ? user.name : 'Loading...'}</div>
}
2. DOM Manipulation
function DocumentTitleChanger({ title }) {
useEffect(() => {
// Side effect: change document title
document.title = title
}, [title]) // Update when title changes
return <h1>Page Content</h1>
}
3. Event Listening
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth)
}
window.addEventListener('resize', handleResize)
// Cleanup: remove event listener when component unmounts
return () => {
window.removeEventListener('resize', handleResize)
}
}, []) // Only need to set up once
return <div>Window width: {width}px</div>
}
π‘ Beyond Side Effects: Custom Hooks & React Query
As your application grows, you'll find yourself repeating useEffect
logic in many places. This is where Custom Hooks shine. You can encapsulate side effect logic into a reusable hook.
For example, creating a useFetch
hook:
function useFetch(url) {
const [data, setData] = useState(null)
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((d) => setData(d))
}, [url])
return data
}
// Usage in component
function MyComponent() {
const userData = useFetch('api/user')
// ...
}
For complex side effects related to server data (caching, re-fetching, optimistic updates...), specialized libraries like TanStack Query (React Query) or SWR are excellent choices, helping you manage side effects powerfully with less code.
Summary: Key Points About Side Effects in React
- Side Effects are any interactions with the "outside world" of your component, such as API calls, DOM manipulation, or timers.
- We must manage side effects to avoid errors like infinite loops and unpredictable behavior.
useEffect
is React's hook for handling side effects in a controlled manner.- Dependency arrays are the heart of
useEffect
, determining when the effect will run again. - Always remember to clean up side effects like subscriptions or timers to avoid memory leaks.
- When logic becomes complex, consider creating Custom Hooks or using libraries like React Query.
Understanding and mastering useEffect
is a turning point in becoming a proficient React developer. Hopefully, this article has helped you successfully "decode" this important concept!