Consider if you can replace useEffect with event handler (We Might Not Need an Effect #2)
kanta1207
Posted on May 7, 2024
What this article series are about 🔍
This article series are basically a summary of You Might Not Need an Effect, which is a React official documentation about the usage of useEffect
hook in React.
The original article is really really good, for sure better than my summary. So, if you have time, I highly recommend you to read the original article.
My motivation is just to provide a quick summary both for myself and for others who might be busy or a bit lazy to read through the whole article.
Yeah, it's for you 😉 haha
This is the second article of the series. And the key takeaway is really simple.
Always consider if you can do it inside of event handler 💡
All of the example covered in this article are about how we can avoid using useEffect
by moving the logic to the event handler.
I'm not covering Passing a data to the parent,
because it's rather about the flow of data in React.
Before diving into them, let's briefly make it clear why we need to reduce useEffect
in the first place.
Why we need to reduce useEffect
in the first place?
- Re-render happens after each
setState
function in each ofuseEffect
- Complexity. It's hard to understand the flow of the code if we have too many
useEffect
So, let's see how we can avoid using useEffect
by moving the logic to the event handler or just during rendering.
Example 1, Bad 👎
When we want to do some shared logic between multiple event handler function, we might be tempted to put the logic inside useEffect
.
But we actually shoudn't do that.
// Let's assume this function is defined elsewhere
const addToCart = (product) => {
product.isInCart = true;
};
function ProductPage({ product, addToCart }) {
// 🔴 Avoid: Event-specific logic inside an Effect
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ...
}
Example 1, Good 👍
If you want to create some shared logic between event handlers, just create a function.
function ProductPage({ product, addToCart }) {
// ✅ Good: Event-specific logic is called from event handlers
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
}
Example 2, Bad 👎
Again, if you can do it on event handler, just do it there rather than using useEffect
.
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Good: This logic should run because the component was displayed, not because of an event
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
// 🔴 Avoid: Event-specific logic inside an Effect. You don't need a state and useEffect for that.
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// ...
}
Example 2, Good 👍
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Good: This logic should run because the component was displayed, not because of an event
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
function handleSubmit(e) {
e.preventDefault();
// ✅ Good: Event-specific logic is in the event handler
post('/api/register', { firstName, lastName });
}
// ...
}
Example 3, Bad 👎
It looks like we're using useEffect
to chain multiple state update each other.
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount((c) => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound((r) => r + 1);
setGoldCardCount(0);
}
}, [goldCardCount]);
useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);
useEffect(() => {
alert('Good game!');
}, [isGameOver]);
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}
}
Example 3, Good 👍 (After we remove all unnecessary useEffect
)
Let's see how we can remove unnecessary useEffect
.
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// const [isGameOver, setIsGameOver] = useState(false); // We don't need this state
// Instead, ✅ calculate what you can during rendering
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}
// ✅ Calculate all the next state in the event handler, rather than in useEffect
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}
}
Takeaway
(Almost) Never use useEffect
for event-specific logic. Just call the logic directly from the event handler function.
I actually couldn't come up with any case where we have to use useEffect
for event-specific logic, please tell me if you can think of any cases.
Reference
See you again in #3!
Posted on May 7, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.