Understanding useEffect in React: Why You Can't Just Call a Function
A Saturday Morning Realization
So it’s a Saturday morning, and I’m basking in the sun, flipping through the last pages of The Psychology of Money by Morgan Housel. I just finished the final chapter. Not gonna lie, I was just floating the whole time because I'm not a US citizen, so understanding their economy with that well-detailed history workflow was very unrelatable. I am in Kenya, so we have completely different economies, but of course, there are some similarities here and there.
Anyway, enough about my side quests. I am here to talk about useEffect
in React. Reading the last chapter felt like reading useEffect
for the first time. When I first encountered it, I didn’t understand why I needed it. Why couldn’t I just call a function inside my component like in regular JavaScript? Say, for example, it's an API call—why not write the function directly in my component? If you’re wondering the same thing, give yourself a little pat on the back because that's a great question. What you need to understand is, React's rendering process is different from the normal execution of JavaScript functions. Let’s break it down.
The Problem: Why Not Just Call a Function?
In JavaScript, you can normally define and call functions inside your code. So why not just do this inside a React component?
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => console.log(data));
return (
<div>
<button onClick={() => setCount((count) => count + 1)}>
I've been clicked {count} times
</button>
<input
type="text"
placeholder="Type away..."
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
);
}
🚨 Issue: Every time the component re-renders (due to state changes like count
or text
updates), the API call runs again. This leads to unnecessary network requests, which slow down your app.
The Waiter Analogy: Explaining Multiple API Calls
Think of this problem like a restaurant scenario:
Without useEffect
: The Overeager Waiter
Imagine you walk into a restaurant and place your order. Now imagine that every time you pick up your spoon or sip water, the waiter rushes back to the kitchen and places another order for you—even though you haven’t asked for it. By the end of the meal, you have ten unnecessary plates on your table.
That’s what happens when you place an API call inside a React component without useEffect
. React re-renders every time state changes, triggering the API call multiple times when it’s not needed.
With useEffect
: The Well-Trained Waiter
Now, imagine a properly trained waiter who only takes your order once when you arrive. They don’t keep reordering unless you explicitly ask for something new.
That’s what useEffect
does—it ensures that side effects like API calls only run when needed, preventing unnecessary re-fetching.
Enter useEffect
: The Solution
To control when our API call runs, we use useEffect
. It consists of two parts:
- A callback function that contains the logic for your side effect.
- A dependency array that determines when the effect should re-run.
Basic Structure:
useEffect(() => {
// Side effect logic
}, [dependencies]);
Example: Fixing Our API Call
import { useEffect, useState } from "react";
function MyComponent() {
const [data, setData] = useState(null);
const [count, setCount] = useState(0);
const [text, setText] = useState("");
useEffect(() => {
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => setData(data));
}, []); // Runs only once when the component mounts
return (
<div>
<div>{data ? JSON.stringify(data) : "Loading..."}</div>
<button onClick={() => setCount((count) => count + 1)}>
I've been clicked {count} times
</button>
<input
type="text"
placeholder="Type away..."
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
);
}
✅ Now, the API call only runs once, just like a well-trained waiter taking your order.
Controlling When useEffect
Runs
You can control useEffect
's behavior using the dependency array:
-
useEffect(() => {})
(No dependency array) → Runs on every render. -
useEffect(() => {}, [])
(Empty dependency array) → Runs only once when the component mounts. -
useEffect(() => {}, [variable])
(Dependency array with values) → Runs whenever the specified variable changes.
Example: Fetch Data When a State Changes
useEffect(() => {
fetch(`https://api.example.com/data?page=${page}`)
.then((response) => response.json())
.then((data) => setData(data));
}, [page]); // Runs only when `page` state changes
Cleaning Up: Avoiding Memory Leaks
Some side effects, like event listeners or timers, need to be cleaned up to prevent memory leaks. useEffect
allows you to return a cleanup function:
useEffect(() => {
const handleResize = () => console.log("Window resized");
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
✅ Now, the event listener is removed when the component unmounts.
What Are Side Effects?
If you're wondering what I mean by side effects, let's break it down:
In a pure function (ideal in React), given the same inputs, you always get the same output. Side effects are actions that occur outside of a component's rendering logic. These include:
- Fetching data from an API
- Modifying the DOM directly
- Setting up event listeners
- Using timers
- Managing subscriptions
Wrapping Up: Full Circle Moment
Just like I pushed through The Psychology of Money despite feeling disconnected from some parts, learning useEffect
felt unnecessary at first—until I understood the why behind it. Now, I see it as an essential tool for managing side effects in React.
Key Takeaways:
✅ Without useEffect
, API calls run on every render, leading to performance issues.
✅ useEffect
lets you control when a function runs, avoiding unnecessary re-renders.
✅ Using the dependency array helps optimize performance.
✅ Cleanup functions prevent memory leaks.
So next time you see an API call inside a React component, think of our overeager waiter. And remember—React re-renders more than you think! 😉
Top comments (3)
Nice read💯💯
Great 👍 I love the analogies there.
Really good explanation