使用通用Action与Reducer简化Redux:实践优劣与架构探讨
Great question—let’s break this down step by step, since these are common pain points when scaling Redux apps.
In short: it works for small prototypes or trivial apps, but is not a sustainable practice for medium-to-large projects. Here’s why, plus the downsides and performance costs:
Key Disadvantages & Performance Drawbacks
- Debugging becomes exponentially harder: Every state update will show up as the same generic action (e.g.,
UPDATE_STATE) in Redux DevTools. You’ll have to dig through payloads to figure out which part of the state was changed, which component triggered it, and why—making it nearly impossible to trace bugs quickly. - Intent is obscured: A generic action typically requires a payload like
{ path: 'user.profile.email', value: 'new@example.com' }. Anyone reading your code has to parse that path to understand what’s being updated, whereas a specific action likeSET_USER_EMAILmakes the intent clear at a glance. - Poor type safety: If you’re using TypeScript, generic actions make strict type checking extremely difficult. You can’t easily validate that the
pathin the payload maps to a valid field in your state, leading to silent bugs from typos or invalid updates. Specific actions let you define precise types for every change. - Unnecessary performance overhead: Generic reducers rely on dynamic path resolution (e.g., using
lodash.setor manual object traversal) to update state. This is slower than a dedicated reducer that directly modifies the relevant state slice—especially as your state tree grows. Additionally, since all updates trigger the same reducer branch, you may end up with more unnecessary component re-renders: components subscribed to unrelated state slices might re-render because the root state object was recreated, even if their specific slice didn’t change.
This approach does have immediate benefits: it consolidates related logic, makes business flows easier to follow at first glance, and keeps testability intact (you can still unit test individual functions in the file). However, it falls apart as your app scales:
Critical Limitations
- Scalability bottlenecks: As your user-related logic grows (e.g., adding login flows, profile editing, order history, preference settings), that single file will balloon into thousands of lines of code. Navigating, modifying, or debugging it becomes a nightmare—especially in team environments where multiple developers might be editing the same file simultaneously.
- Violates separation of concerns: Redux’s core design separates intent (actions), state updates (reducers), and side effects (middleware like thunks). Shoving all logic into one file mixes these concerns together, creating tight coupling that makes it hard to refactor, split, or reuse parts of the logic later.
- Reusability issues: If another part of your app needs a small piece of user-related logic (e.g., formatting a user’s full name), you’ll either have to extract it from the monolithic file (adding overhead) or import the entire file, leading to unnecessary code bloat. A modular approach (with separate selectors, utility functions, and action creators) lets you reuse small, focused pieces of logic across the app.
- Incompatibility with Redux tooling: Modern Redux tooling like Redux Toolkit (RTK) is built around the standard action/reducer pattern. Features like
createAsyncThunk,createSlice, or RTK Query rely on structured actions and reducers to provide auto-generated logic, immutability helpers, and performance optimizations. A single-file monolith can’t leverage these tools, forcing you to write more custom code and miss out on built-in best practices.
That said, if you’re working on a small, short-lived project or a simple feature, this single-file approach might be acceptable. But for apps that need to grow and be maintained long-term, sticking to modular, concern-separated Redux patterns (ideally with RTK to reduce boilerplate) is the better choice.
内容的提问来源于stack exchange,提问作者Maria

