Blog #27: useEffect is not the place for doing all synchronizing logic
I used to use useEffect as a 'cure-all' to synchronize data. The result was me falling into a trap called Dependency Hell.
I admit I was once a useEffect addict. When React Hooks was first released, I thought it was the greatest invention to replace traditional lifecycles. I believed that: "Whenever variable A changes and I want to do thing B, just toss it into useEffect."
I believed it because it provided a sense of being "reactive." Looking at the code seemed very smart: "If userId changes, fetch data. If data arrives, calculate the total cost. If total changes, show a notification."
But reality taught me a lesson I won't forget while building a cart and checkout system.
1. Context: Chain Reaction
My system had a sequence of useEffect hooks linked indirectly through states.
- User clicks "Apply discount code."
useEffectobservesdiscountCodeand calls API for the discount value.- Once the discount value is received, another
useEffectobservesdiscountValueto recalculatetotalPrice. - When
totalPricechanges, anotheruseEffectchecks if the user is eligible for Freeship.
The result: A single click by the user triggered 3-4 consecutive re-renders. In some cases, it even created an infinite loop and froze the browser. Debugging this was like untangling a mess of headphone wires in the dark.
2. Concept: Event Handlers vs Side Effects
My fundamental mistake was using useEffect (Side Effect) for tasks that belonged to Event Handlers.
// How it sounded "correct" (but was bad)
useEffect(() => {
if (cartItems) {
const total = calculateTotal(cartItems);
setTotal(total);
}
}, [cartItems]); // Why wait for render to finish just to calculate the total?
Why is it bad? Because useEffect runs after the browser has finished painting the screen. This means the browser paints once (without total), then runs the effect, then re-renders to show the total. The user will see a "stutter" in the figures.
3. Practical Approach and Balance
A Junior will also often cling to useEffect because it's easy to understand as "if... then...". But in reality, most logic can be resolved right during the render calculation or at the event handler function.
Comparison of two ways:
- Using useEffect: Render -> Run Effect -> Re-render. Scattered code, harder to trace data flow. Laggy.
- Using Event Handler / Derived State:
- Calculate
totaldirectly fromcartItemsright in the function component body (Derived State). - Actions like "Calculate discount" should sit right in the
onApplyDiscountfunction (Event Handler).
- Calculate
Lesson Learned
I learned that: Don't use Side Effects to synchronize state that you can calculate directly from current props/state.
- When the old way is still correct: Synchronizing with external systems (API, Subscription, direct DOM, Sockets).
- When to avoid: Avoid using
useEffectjust to change state based on another state. Calculate it directly or handle it in theonEventfunction.
The cleanest code is code where you don't need to use useEffect.
Notes on the day I stopped chasing dependency ghosts.
Series • Part 27 of 50