LogoTRUONG PHAM
Home
Projects
Portfolio
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
  • Portfolio
  • Blogs
  • YouTube
  • Contact

Connect

  • GitHub
  • LinkedIn
  • Email
  • YouTube

© 2024 TRUONG PHAM. © All rights reserved.

Privacy PolicyTerms of Service
Back/50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES
Blog #21: The Global State Nightmare – The decision to 'topple' the Redux monument

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.

May 10, 2024·4 min read

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.

Previous: Blog #20: The Mystery of LCP – Why do 'fast' websites still get rated low?All posts in this seriesNext: Blog #22: Click Race Condition – When User's click speed beats your App