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.
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
reselectfor every selector, applyRedux Toolkitto 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:
- Server State: Use React Query to handle data from the API.
- Local UI State: Use
useState/useReducerfor individual components. - 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