The straightforward answer to the question is, We often find ourselves writing brute force code. Sometimes we are tempted to copy codes without understanding the fundamentals.
I think, instead of asking “Why we should not….” it is more proper to ask “How we should not…” or “Where we should not….”
What is an Effect?
React documentation literature says, Effects are an escape hatch from the React paradigm. Well, what do they mean by that? Effects in React let you connect your components with things outside React. Like non-React widgets or the browser. Generally we use useEffect either for transforming data for rendering or handling user events.
We don’t have non-React widgets on the server. And that's why we don’t have useEffect in the server component. Does that ring a bell?
If you're just updating a component based on its own state or props, you don't need an Effect. useEffect introduces additional computations and overhead to your component lifecycle. Interesting thing is, useEffect is not inherently slower. Improper usage of useEffect can potentially introduce performance issues.
And as the rule of state- management-101, “Do not replicate!”
So let's have some implementation details where we can avoid useEffect.
Updating state based on props or state:
When something can be calculated from the existing props or state, don’t put it in state. Instead, calculate it during rendering.
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
This is more complicated than necessary. It is inefficient too: it does an entire render pass with a stale value for fullName, then immediately re-renders with the updated value. Instead this can be done,
// ✅ this is calculated during rendering
const fullName = firstName + ' ' + lastName;
Or we can give the values to the dom
// ✅ Will re-render
<>{firstName} {lastName}</>
Child component always re-renders if the prop value is changed. So what if we have to reset all component states depending on prop value, how do we do that?
For example we want to clear out the comment state variable (or refetch from state) whenever the userId changes:
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 Avoid: Resetting state on prop change in an Effect
useEffect(() => {
setComment(''); // or have a selector
}, [userId]);
// ...
}
Instead of doing this, we can split the component and pass a unique key prop to the Profile component,
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ This and any other state below will reset on key change automatically
const [comment, setComment] = useState('');
// ...
}
Again, why do this? Because by passing userId as a key to the Profile component, you’re asking React to treat two Profile components with different userId as two different components that should not share any state. Whenever the key (which you’ve set to userId) changes, React will recreate the DOM and reset the state of the Profile component and all of its children. Now the comment field will clear out automatically when navigating between profiles.
What if we want to adjust some of the state depending on props value but not all of it.
For example we might tempted to do this,
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🔴 Avoid: Adjusting state on prop change in an Effect
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
Again, this is not ideal. Every time the items change, the List and its child components will render with a stale selection value at first.
Instead we can do this,
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// Better: Adjust the state while rendering
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
Storing information from previous renders like this can be hard to understand, but it’s better than updating the same state in an Effect. Although this pattern is more efficient than an Effect, most components shouldn’t need it either. It’s better to check whether we can reset all states with a key or calculate everything during rendering instead.
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Best: Calculate everything during rendering
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}
Sharing logic between event handlers:
Let’s say you have a product page with two buttons (Buy and Checkout) that both let you buy that product. You want to show a notification whenever the user puts the product in the cart. Calling showNotification() in both buttons’ click handlers feels repetitive so you might be tempted to place this logic in an Effect:
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');
}
// ...
}
This Effect is unnecessary. It will also most likely cause bugs. For example, let’s say that your app “remembers” the shopping cart between the page reloads. If you add a product to the cart once and refresh the page, the notification will appear again.
Better we can write event specific methods
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');
}
// ...
}
This both removes the unnecessary Effect and fixes the bug.
Chains of computations
This is one of the most common mistakes we make. Sometimes there can be a case where we need to change one state depending on the other state in the component.
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
// 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
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);
}
}
the component (and its children) have to re-render between each set call in the chain. In the example above, in the worst case (setCard → render → setGoldCardCount → render → setRound → render → setIsGameOver → render) there are three unnecessary re-renders of the tree below.
Even if performance is not the case, as our code evolves, it gets harder and harder to maintain and add new requirements to the code.
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// ✅ 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
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}
// ...
Caching expensive calculations:
They are recommending to use useMemo instead of useEffect. But from React 19, they do not suggest useMemo anymore. Not sure how to solve it another way. No worries, we will see (If you know, please share).
Notifying parent components about state changes or passing data to the parent:
Passing data to higher order components is the root of all EVIL. So let’s not think about ‘passing data to the parent’ at all. For your own sake use state management libraries.
Footnote:
So the elephant in the room, Why follow these approaches?
In my opinion all the actions, interactions, dependencies, lifecycle, and all possible approaches to solve a particular problem, breaks down to one or another data structure.
So why is it that some of the data structure is faster and some are slower, why is that?
Honestly. I don’t know.
There is something called time space trade off. This problem is as old as computer engineering itself. This is something all programmers have to deal with. So it is important to follow standard norms by understanding the underlying mechanism of our code.
But we are talking about useEffect here, Aren’t we? We do not think about memories while writing. We don’t manage garbage values, and don't have to think about abstract stuff. React does all the heavy lifting.
But do you know, React does not even understand “jsx”? 🙂
Top comments (0)