LogoTRUONG PHAM
Home
Projects
Blogs
YouTube
Contact

Newsletter

Stay updated with technical artifacts and engineering insights.

LogoTRUONG PHAM

Building scalable software and sharing insights on technology & life.

Sitemap

  • Home
  • Projects
  • Blogs
  • YouTube
  • Contact

Connect

  • GitHub
  • LinkedIn
  • Email
  • YouTube

© 2024 TRUONG PHAM. © All rights reserved.

Privacy PolicyTerms of Service
Back
Blog #27: useEffect is not the place for doing all synchronizing logic
50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

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.

TP
Truong PhamSoftware Engineer
PublishedJune 5, 2024

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.

  1. User clicks "Apply discount code."
  2. useEffect observes discountCode and calls API for the discount value.
  3. Once the discount value is received, another useEffect observes discountValue to recalculate totalPrice.
  4. When totalPrice changes, another useEffect checks 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 total directly from cartItems right in the function component body (Derived State).
    • Actions like "Calculate discount" should sit right in the onApplyDiscount function (Event Handler).

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 useEffect just to change state based on another state. Calculate it directly or handle it in the onEvent function.

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

50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

NextBlog #28: The naivety of believing setState is an immediate assignment
Blog #26: Don't let 'Single Source of Truth' become blind dogma
22Blog #22: Click Race Condition – When User's click speed beats your App23Blog #23: Multi-tab Syncing – When your application 'talks' between tabs24Blog #24: Failed Optimistic Update – When efforts to 'smooth' UI turn into UX disasters25Blog #25: Context API Perf Hit – When the 'standard React' solution betrays you26Blog #26: Don't let 'Single Source of Truth' become blind dogma27Blog #27: useEffect is not the place for doing all synchronizing logicReading28Blog #28: The naivety of believing setState is an immediate assignment29Blog #29: When 'Controlled Component' turns into a performance burden30Blog #30: A giant State Object doesn't make your code cleaner31Blog #31: 1 AM and the Search for the 'Missing' File32Blog #32: Friday Afternoon, 800 Lines of Code, and a Miscalculation
TP

Written by Truong Pham

Software Engineer passionate about building high-performance systems and meaningful experiences.

Read more articles