React状态管理方案迁移复盘:从Redux到Zustand的渐进式替换经验

React状态管理方案迁移复盘:从Redux到Zustand的渐进式替换经验

一、Redux的"重"在哪里

一个React中后台项目使用Redux Toolkit管理全局状态。2年下来,Redux代码膨胀到可用性问题:

  • 12个slice文件,平均每个200行
  • Store初始化需要配置5个中间件(thunk、logger、persist、devtools、immutability check)
  • 新增一个状态字段需要修改5个文件:slice、type、action、selector、component

"做一个下拉框的联动"需要约60行Redux代码。问题的本质:Redux给了我们一套严谨的状态管理方法论,但95%的状态场景不需要这么严谨。

迁移目标:把Redux Store中的状态逐步迁移到Zustand,保持类型安全,保持中间件支持(persist、devtools)。

二、迁移策略:不是替换,而是共存

原则一:新模块直接用Zustand,不动旧代码。订单管理模块是新开发的,直接从Zustand开始,不与Redux交互。

原则二:旧Store按"依赖少→依赖多"的顺序迁移。UI Store(主题、侧边栏)不依赖任何其他Store,最先迁移。Auth Store被多处引用,最后迁移。

// 迁移前:Redux UI Slice —— 150行 // store/uiSlice.ts const uiSlice = createSlice({ name: 'ui', initialState: { sidebarCollapsed: false, theme: 'light' as 'light' | 'dark', loading: false, }, reducers: { toggleSidebar(state) { state.sidebarCollapsed = !state.sidebarCollapsed; }, setTheme(state, action: PayloadAction<'light' | 'dark'>) { state.theme = action.payload; }, setLoading(state, action: PayloadAction<boolean>) { state.loading = action.payload; }, }, }); // 迁移后:Zustand Store —— 60行 // stores/useUIStore.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface UIState { sidebarCollapsed: boolean; theme: 'light' | 'dark'; loading: boolean; toggleSidebar: () => void; setTheme: (theme: 'light' | 'dark') => void; setLoading: (loading: boolean) => void; } export const useUIStore = create<UIState>()( persist( (set) => ({ sidebarCollapsed: false, theme: 'light', loading: false, toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })), setTheme: (theme) => set({ theme }), setLoading: (loading) => set({ loading }), }), { name: 'ui-store' } // localStorage持久化 ) );

代码量从150行降到60行(60%减少)。不需要Provider包裹、不需要dispatch、不需要actionCreator。

组件中使用的变化:

// Redux 方式 const dispatch = useDispatch(); const sidebarCollapsed = useSelector((state: RootState) => state.ui.sidebarCollapsed); dispatch(toggleSidebar()); // Zustand 方式 const { sidebarCollapsed, toggleSidebar } = useUIStore();

三、中间件:Zustand如何替代Redux中间件

持久化(persist):Zustand的persist中间件比redux-persist简单得多——不需要PersistGate组件。

开发者工具(devtools):添加一行即可:

import { devtools } from 'zustand/middleware'; export const useAuthStore = create<AuthState>()( devtools( persist((set) => ({...})), { name: 'auth-store' } ) );

不可变性检查(immer):

import { immer } from 'zustand/middleware/immer'; export const useOrderStore = create<OrderState>()( immer((set) => ({ items: [], addItem: (item) => set((state) => { state.items.push(item); // 可以直接mutate,immer处理不可变性 }), })) );

四、迁移中的坑

坑1:Redux和Zustand共存的DevTools混乱。当两者同时运行时,Redux DevTools中只能看到Redux的action。解决方案:给Zustand的devtools指定不同的name,在DevTools的实例切换中查看。

坑2:中间件的执行顺序敏感。Zustand的中间件是从右到左执行的(compose)。devtools(persist(store))persist(devtools(store))的行为不同。正确顺序:devtools(persist(...))——先执行persist加载本地数据,devtools在外部记录。

坑3:selector的性能差异。Redux的useSelector有内置的浅比较优化。Zustand的useStore(selector)默认使用Object.is,需要手动提供shallow比较函数:

import { shallow } from 'zustand/shallow'; // 不好——每次返回新引用导致重渲染 const { name, age } = useUserStore(); // 好——用shallow比较 const { name, age } = useUserStore( (state) => ({ name: state.name, age: state.age }), shallow );

五、总结

从Redux到Zustand的核心体验:

  • Zustand的代码量平均是Redux的40-50%。不是因为"Zustand更聪明",而是因为Redux设计了很多非必须的抽象层。
  • 不需要Provider是最大的使用体验提升——少了顶层Provider的包裹,少了测试环境的Store Mock。
  • 迁移一定不能"大爆炸"。Redux和Zustand可以共存,按模块渐进迁移。
  • 中间件生态系统Zustand虽然不如Redux丰富,但核心场景(persist、devtools、immer)都有官方支持。

迁移完成后:Redux Store从12个slice减少到1个(权限管理保留),Zustand Store新增7个。开发者新增一个状态管理单元的时间从约30分钟降到10分钟。最大的隐性收益是:新成员不需要学习Redux的action/reducer/dispatch概念——直接看Zustand的Store,useStore,取值改值。学习成本降低约60%。