DEV Community

Cover image for Implementation Mistakes by Devs: How you should not use useEffect
Shibly Noman
Shibly Noman

Posted on

Implementation Mistakes by Devs: How you should not use useEffect

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]);
 // ...
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Or we can give the values to the dom

// ✅ Will re-render
 <>{firstName} {lastName}</>
Enter fullscreen mode Exit fullscreen mode

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]);
 // ...
}
Enter fullscreen mode Exit fullscreen mode

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('');
 // ...
}
Enter fullscreen mode Exit fullscreen mode

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]);
 // ...
}
Enter fullscreen mode Exit fullscreen mode

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);
 }
 // ...
}
Enter fullscreen mode Exit fullscreen mode

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;
 // ...
}
Enter fullscreen mode Exit fullscreen mode

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');
 }
 // ...
}
Enter fullscreen mode Exit fullscreen mode

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');
 }
 // ...
}
Enter fullscreen mode Exit fullscreen mode

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);
   }
 }
Enter fullscreen mode Exit fullscreen mode

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!');
       }
     }
   }
 }

 // ...
Enter fullscreen mode Exit fullscreen mode

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)