前言
React 状态管理一直是前端开发者绕不开的话题。从早期的 Redux 一统江湖,到如今 Zustand、Jotai 等轻量方案崛起,开发者面临的选择越来越多。但选择多了,反而容易陷入「选择困难症」。
本文将带你亲手实现一个电商购物车功能,分别用 Zustand、Jotai 和 Redux 三种方案完成,并对比它们的代码量、性能、可维护性及学习曲线。读完本文,你将能根据项目需求做出最合适的选型。
项目准备
我们先创建一个 React 项目(使用 Vite + TypeScript):
npm create vite@latest state-compare -- --template react-ts
cd state-compare
npm install
然后安装三个状态管理库:
npm install zustand jotai @reduxjs/toolkit react-redux
场景:购物车
我们需要实现的功能:
- 商品列表(从 API 获取)
- 添加商品到购物车
- 购物车数量加减
- 删除商品
- 计算总价
我们将在 src/App.tsx 中统一渲染,但状态管理逻辑分别放在不同文件夹中。
方案一:Zustand
Zustand 是一个极简的状态管理库,核心是一个 hook 式的 store。
1. 创建 Store
创建 src/zustand/store.ts:
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: number) => void;
updateQuantity: (id: number, quantity: number) => void;
clearCart: () => void;
total: () => number;
}
export const useCartStore = create<CartState>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => {
const items = get().items;
const existing = items.find(i => i.id === item.id);
if (existing) {
set({ items: items.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i) });
} else {
set({ items: [...items, { ...item, quantity: 1 }] });
}
},
removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })),
updateQuantity: (id, quantity) => set((state) => ({
items: state.items.map(i => i.id === id ? { ...i, quantity: Math.max(1, quantity) } : i)
})),
clearCart: () => set({ items: [] }),
total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
}),
{ name: 'cart-storage' }
)
);
> 💡 使用 persist 中间件可以自动将购物车数据持久化到 localStorage。
2. 在组件中使用
创建 src/zustand/Cart.tsx:
import { useCartStore } from './store';
export function Cart() {
const { items, addItem, removeItem, updateQuantity, clearCart, total } = useCartStore();
return (
<div>
<h2>Zustand 购物车</h2>
{items.length === 0 ? <p>购物车为空</p> : (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} - ${item.price} x {item.quantity}
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
<button onClick={() => removeItem(item.id)}>删除</button>
</li>
))}
</ul>
)}
<p>总价: ${total()}</p>
<button onClick={clearCart}>清空购物车</button>
</div>
);
}
Zustand 的优势在于:API 简洁,无需 Provider 包裹,直接在任何组件中调用 hook 即可。性能方面,Zustand 默认使用 Object.is 进行浅比较,只在相关状态变化时重新渲染。
方案二:Jotai
Jotai 采用原子化状态管理,每个状态是一个独立的 atom,通过组合实现复杂逻辑。
1. 创建 atoms
创建 src/jotai/store.ts:
import { atom } from 'jotai';
export interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
export const cartItemsAtom = atom<CartItem[]>([]);
export const addItemAtom = atom(
null,
(get, set, item: Omit<CartItem, 'quantity'>) => {
const items = get(cartItemsAtom);
const existing = items.find(i => i.id === item.id);
if (existing) {
set(cartItemsAtom, items.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i));
} else {
set(cartItemsAtom, [...items, { ...item, quantity: 1 }]);
}
}
);
export const removeItemAtom = atom(
null,
(get, set, id: number) => {
set(cartItemsAtom, get(cartItemsAtom).filter(i => i.id !== id));
}
);
export const updateQuantityAtom = atom(
null,
(get, set, { id, quantity }: { id: number; quantity: number }) => {
set(cartItemsAtom, get(cartItemsAtom).map(i => i.id === id ? { ...i, quantity: Math.max(1, quantity) } : i));
}
);
export const totalAtom = atom((get) => {
const items = get(cartItemsAtom);
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
});
> 💡 Jotai 的 atom 可以定义读写逻辑,派生 atom(如 totalAtom)会自动缓存,只有依赖变化时才重新计算。
2. 在组件中使用
创建 src/jotai/Cart.tsx:
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { cartItemsAtom, addItemAtom, removeItemAtom, updateQuantityAtom, totalAtom } from './store';
export function Cart() {
const items = useAtomValue(cartItemsAtom);
const addItem = useSetAtom(addItemAtom);
const removeItem = useSetAtom(removeItemAtom);
const updateQuantity = useSetAtom(updateQuantityAtom);
const total = useAtomValue(totalAtom);
return (
<div>
<h2>Jotai 购物车</h2>
{items.length === 0 ? <p>购物车为空</p> : (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} - ${item.price} x {item.quantity}
<button onClick={() => updateQuantity({ id: item.id, quantity: item.quantity - 1 })}>-</button>
<button onClick={() => updateQuantity({ id: item.id, quantity: item.quantity + 1 })}>+</button>
<button onClick={() => removeItem(item.id)}>删除</button>
</li>
))}
</ul>
)}
<p>总价: ${total}</p>
</div>
);
}
Jotai 的优势在于:颗粒度细,只有使用特定 atom 的组件才会重新渲染。对于大型应用,可以避免不必要的渲染。同时,Jotai 不需要 Provider,但支持 Context 作用域。
方案三:Redux Toolkit
Redux Toolkit 是 Redux 官方推荐的写法,简化了样板代码。
1. 创建 Slice
创建 src/redux/store.ts:
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
}
const initialState: CartState = {
items: [],
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem: (state, action: PayloadAction<Omit<CartItem, 'quantity'>>) => {
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
},
removeItem: (state, action: PayloadAction<number>) => {
state.items = state.items.filter(i => i.id !== action.payload);
},
updateQuantity: (state, action: PayloadAction<{ id: number; quantity: number }>) => {
const item = state.items.find(i => i.id === action.payload.id);
if (item) {
item.quantity = Math.max(1, action.payload.quantity);
}
},
clearCart: (state) => {
state.items = [];
},
},
});
export const { addItem, removeItem, updateQuantity, clearCart } = cartSlice.actions;
export const store = configureStore({
reducer: {
cart: cartSlice.reducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
2. 创建 hooks 和 Provider
创建 src/redux/hooks.ts:
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
在 src/main.tsx 中包裹 Provider:
import { Provider } from 'react-redux';
import { store } from './redux/store';
ReactDOM.createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<App />
</Provider>
);
3. 在组件中使用
创建 src/redux/Cart.tsx:
import { useAppSelector, useAppDispatch } from './hooks';
import { addItem, removeItem, updateQuantity, clearCart } from './store';
export function Cart() {
const items = useAppSelector(state => state.cart.items);
const dispatch = useAppDispatch();
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return (
<div>
<h2>Redux 购物车</h2>
{items.length === 0 ? <p>购物车为空</p> : (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} - ${item.price} x {item.quantity}
<button onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity - 1 }))}>-</button>
<button onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity + 1 }))}>+</button>
<button onClick={() => dispatch(removeItem(item.id))}>删除</button>
</li>
))}
</ul>
)}
<p>总价: ${total}</p>
<button onClick={() => dispatch(clearCart())}>清空购物车</button>
</div>
);
}
Redux 的优势在于:生态丰富,中间件(如 redux-saga、redux-thunk)成熟,适合大型团队和复杂状态逻辑。但样板代码相对较多。
性能对比
我们使用 React DevTools 的 Profiler 来测量渲染性能。在添加商品时,三种方案的表现:
- Zustand:只有使用 store 的组件重新渲染,但若组件订阅了整个 store,即使只改变部分状态也会重渲染。建议使用 selector 精确选择。
- Jotai:只重渲染使用了变化 atom 的组件,颗粒度最细,性能最佳。
- Redux:通过
useSelector可以精确选择,但 Provider 模式可能导致根组件重渲染。使用React.memo可优化。
> 💡 对于大多数中小型应用,性能差异可以忽略。但在大型应用中,Jotai 的原子化方案能有效减少不必要的渲染。
选型建议
| 维度 | Zustand | Jotai | Redux Toolkit | |——|———|——-|—————| | 学习曲线 | ⭐ (低) | ⭐⭐ (中) | ⭐⭐⭐ (高) | | 样板代码 | 少 | 少 | 中 | | 性能 | 好 | 优秀 | 好 | | 生态 | 一般 | 一般 | 丰富 | | 适合场景 | 中小型应用、快速原型 | 需要细粒度控制、复杂派生状态 | 大型应用、团队协作、复杂异步逻辑 |
我的建议:
- 如果你追求极简和开发效率,选 Zustand。
- 如果你需要精细化的状态管理,尤其是派生状态多,选 Jotai。
- 如果你是大型团队或需要强大的中间件支持,选 Redux Toolkit。
总结
本文通过购物车案例,完整演示了 Zustand、Jotai 和 Redux Toolkit 三种状态管理方案的实现。每种方案各有优劣,选择时应根据项目规模、团队经验和具体需求来定。希望这篇教程能帮助你做出明智的选型决策。
延伸阅读:
- Zustand 官方文档:https://github.com/pmndrs/zustand
- Jotai 官方文档:https://jotai.org/
- Redux Toolkit 官方文档:https://redux-toolkit.js.org/