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 #21: The Global State Nightmare – The decision to 'topple' the Redux monument
50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

Blog #21: The Global State Nightmare – The decision to 'topple' the Redux monument

Analyzing the decision to restructure the data flow for a large-scale E-commerce system with 15 developers and thousands of products.

TP
Truong PhamSoftware Engineer
PublishedMay 10, 2024

I still vividly remember the context of that project: An E-commerce platform in a period of intense transformation. The team size had grown to 15 people, and the deadline for the "Big Update" was only 3 months away. The system scale had ballooned out of control with hundreds of Actions, Reducers, and tangled data flows in Redux.

The Problem: When "Everything is Global" becomes a barrier

At that time, our team encountered a serious problem: Everything was pushed into the global Redux Store. From cart contents to the open/closed state of a small menu, or even the value being typed in a search box.

The consequences were:

  • Lagging Performance: Every small action caused half the application to re-render because Connect/Selector wasn't optimized.
  • Maintenance Burden: A Junior developer joining the team spent 2-3 weeks just trying to understand why changing a variable on page A broke the logic on page B.
  • Slow Development Speed: Writing Boilerplate for every new feature took too much time.

Options Considered

We sat down and came up with two main directions:

Option 1: Keep Redux and optimize strictly (The Traditionalist)

  • Solution: Use reselect for every selector, apply Redux Toolkit to reduce boilerplate, and train the technical team to write Middleware.
  • Pros: Doesn't change the original architecture; low system integration risk.
  • Cons: Still doesn't solve the "everything is global" problem. Boilerplate remains.

Option 2: Restructure into a Hybrid State model (The Modernist)

  • Solution: Separate data into 3 layers:
    1. Server State: Use React Query to handle data from the API.
    2. Local UI State: Use useState/useReducer for individual components.
    3. Global Shared State: Use Zustand (or a stripped-down Redux) only for information that truly needs to be shared (Auth, Cart).
  • Pros: Distinctly optimal performance, clean code, easy to modularize.
  • Cons: Changes the whole team's mindset; takes time to refactor old modules.

Final Decision and Analysis

After conducting a small proof of concept (PoC), I decided to choose Option 2.

// Example of the new structure after separation
// 1. Server State (Easy caching and loading management)
const { data, isLoading } = useQuery(['products'], fetchProducts);

// 2. Local State (Isolates re-renders)
const [isOpen, setIsOpen] = useState(false);

// 3. Global State (Keep only the core)
const useAuth = create((set) => ({
  user: null,
  login: (userData) => set({ user: userData }),
}));

Impact on Performance: Our Lighthouse score jumped from 65 to 85. Re-renders were completely isolated. When a user types a search, only the input box and the results list re-render—no more system-wide "earthquakes."

Impact on Maintainability: The codebase became more "modular." Data fetching logic no longer resided sporadically in Redux Actions/Effects but was concentrated in the Service/Hooks layer.

Impact on Team: The Juniors were initially a bit confused by React Query, but after 1 week, they reported: "Handling loading/error is much easier now; I don't have to write 3 actions for Start/Success/Fail anymore."

Self-Reflection: Was it Over-engineering?

Sometimes I wonder: Was tearing down and rebuilding such a large part of the architecture in 3 months too risky? In reality, if we had only optimized the old Redux, the app would still run. But in the long run, Technical Debt would have killed the project as the scale doubled.

If I went back to that time, I would still choose this option, but perhaps I would plan the transition more thoroughly (Incremental Migration) to ease the pressure on the team in the first month. No solution is divine; only the most suitable solution for the system's endurance threshold and the team's capacity.


Notes on a "barrier-crossing" event to find peace for the source code.

Series • Part 21 of 50

50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

NextBlog #22: Click Race Condition – When User's click speed beats your App
Blog #20: The Mystery of LCP – Why do 'fast' websites still get rated low?
16Blog #16: Misplaced Debounce – When a localized solution becomes a system burden17Blog #17: Images vs JS – The battle for priority in the Critical Path18Blog #18: Laggy Animations – When the touch is no longer smooth19Blog #19: Chrome Performance Tab – Looking through a microscope at the system's pulse20Blog #20: The Mystery of LCP – Why do 'fast' websites still get rated low?21Blog #21: The Global State Nightmare – The decision to 'topple' the Redux monumentReading22Blog #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 dogma
TP

Written by Truong Pham

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

Read more articles