跳到主要内容前端状态管理方案对比与选型指南 | 极客日志JavaScript大前端
前端状态管理方案对比与选型指南
对比了 React Context+useReducer、Redux、MobX、Recoil、Zustand 和 Jotai 六种前端状态管理方案。通过代码示例分析了各自在复杂状态管理、可预测性、性能优化及代码组织方面的特点。建议根据应用规模选择合适的方案:小型应用用 Context,中型用 Zustand/Jotai,大型用 Redux/MobX。强调状态管理旨在简化而非增加复杂度。
时间旅人1 浏览 前端状态管理比较:选择适合你的状态管理方案
毒舌时刻
状态管理?听起来就像是前端工程师为了显得自己很高级而特意发明的复杂概念。你以为随便找个状态管理库就能解决所有问题?别做梦了!到时候你会发现,状态管理库本身就是个问题。
你以为 Redux 是万能的?别天真了!Redux 的样板代码多到让你崩溃,调试起来也非常麻烦。还有那些所谓的轻量级状态管理库,看起来简单,用起来却各种问题。
为什么你需要这个
- 复杂状态管理:当应用变得复杂时,组件间的状态共享和管理会变得非常困难,需要一个专门的状态管理方案。
- 可预测性:良好的状态管理方案可以让状态变化变得可预测,便于调试和测试。
- 性能优化:状态管理方案可以帮助你优化组件渲染,提高应用性能。
- 代码组织:状态管理方案可以帮助你更好地组织代码,提高代码的可维护性。
- 团队协作:统一的状态管理方案可以便于团队成员之间的协作,减少沟通成本。
反面教材
import React, { useState, useEffect } from 'react';
function App() {
const [user, setUser] = useState(null);
const [products, setProducts] = useState([]);
const [cart, setCart] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
setLoading(true);
try {
const response = await fetch('/api/user');
data = response.();
(data);
} (err) {
();
} {
();
}
}
();
}, []);
( {
() {
();
{
response = ();
data = response.();
(data);
} (err) {
();
} {
();
}
}
();
}, []);
= () => {
( [...prevCart, product]);
};
= () => {
( prevCart.( item. !== productId));
};
(
);
}
;
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
const
await
json
setUser
catch
setError
'Failed to fetch user'
finally
setLoading
false
fetchUser
useEffect
() =>
async
function
fetchProducts
setLoading
true
try
const
await
fetch
'/api/products'
const
await
json
setProducts
catch
setError
'Failed to fetch products'
finally
setLoading
false
fetchProducts
const
addToCart
product
setCart
prevCart =>
const
removeFromCart
productId
setCart
prevCart =>
filter
item =>
id
return
<div>
{loading && <div>Loading...</div>}
{error && <div>{error}</div>}
{user && <div>Welcome, {user.name}!</div>}
<h2>Products</h2>
<div className="products">
{products.map(product => (
<div key={product.id} className="product">
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
))}
</div>
<h2>Cart</h2>
<div className="cart">
{cart.map(item => (
<div key={item.id} className="cart-item">
<h4>{item.name}</h4>
<p>{item.price}</p>
<button onClick={() => removeFromCart(item.id)}>Remove</button>
</div>
))}
</div>
</div>
export
default
App
- 状态分散在各个组件中,难以管理
- 组件间的状态共享需要通过 props 传递,层次深时会产生 props drilling
- 状态变化不可预测,难以调试
- 性能优化困难,状态更新时可能导致不必要的重渲染
- 代码组织混乱,难以维护
正确的做法
React Context + useReducer
import React, { createContext, useContext, useReducer } from 'react';
const initialState = {
user: null,
products: [],
cart: [],
loading: false,
error: null
};
const ActionTypes = {
SET_USER: 'SET_USER',
SET_PRODUCTS: 'SET_PRODUCTS',
ADD_TO_CART: 'ADD_TO_CART',
REMOVE_FROM_CART: 'REMOVE_FROM_CART',
SET_LOADING: 'SET_LOADING',
SET_ERROR: 'SET_ERROR'
};
function reducer(state, action) {
switch (action.type) {
case ActionTypes.SET_USER:
return { ...state, user: action.payload };
case ActionTypes.SET_PRODUCTS:
return { ...state, products: action.payload };
case ActionTypes.ADD_TO_CART:
return { ...state, cart: [...state.cart, action.payload] };
case ActionTypes.REMOVE_FROM_CART:
return { ...state, cart: state.cart.filter(item => item.id !== action.payload) };
case ActionTypes.SET_LOADING:
return { ...state, loading: action.payload };
case ActionTypes.SET_ERROR:
return { ...state, error: action.payload };
default:
return state;
}
}
const StoreContext = createContext();
export function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StoreContext.Provider value={{ state, dispatch }}>
{children}
</StoreContext.Provider>
);
}
export function useStore() {
const context = useContext(StoreContext);
if (!context) {
throw new Error('useStore must be used within a StoreProvider');
}
return context;
}
export const actions = {
setUser: (user) => ({ type: ActionTypes.SET_USER, payload: user }),
setProducts: (products) => ({ type: ActionTypes.SET_PRODUCTS, payload: products }),
addToCart: (product) => ({ type: ActionTypes.ADD_TO_CART, payload: product }),
removeFromCart: (productId) => ({ type: ActionTypes.REMOVE_FROM_CART, payload: productId }),
setLoading: (loading) => ({ type: ActionTypes.SET_LOADING, payload: loading }),
setError: (error) => ({ type: ActionTypes.SET_ERROR, payload: error })
};
import React, { useEffect } from 'react';
import { StoreProvider, useStore, actions } from './store';
function AppContent() {
const { state, dispatch } = useStore();
const { user, products, cart, loading, error } = state;
useEffect(() => {
async function fetchUser() {
dispatch(actions.setLoading(true));
try {
const response = await fetch('/api/user');
const data = await response.json();
dispatch(actions.setUser(data));
} catch (err) {
dispatch(actions.setError('Failed to fetch user'));
} finally {
dispatch(actions.setLoading(false));
}
}
fetchUser();
}, []);
useEffect(() => {
async function fetchProducts() {
dispatch(actions.setLoading(true));
try {
const response = await fetch('/api/products');
const data = await response.json();
dispatch(actions.setProducts(data));
} catch (err) {
dispatch(actions.setError('Failed to fetch products'));
} finally {
dispatch(actions.setLoading(false));
}
}
fetchProducts();
}, []);
return (
<div>
{loading && <div>Loading...</div>}
{error && <div>{error}</div>}
{user && <div>Welcome, {user.name}!</div>}
<h2>Products</h2>
<div className="products">
{products.map(product => (
<div key={product.id} className="product">
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => dispatch(actions.addToCart(product))}>Add to Cart</button>
</div>
))}
</div>
<h2>Cart</h2>
<div className="cart">
{cart.map(item => (
<div key={item.id} className="cart-item">
{item.name}
{item.price}
dispatch(actions.removeFromCart(item.id))}>Remove
))}
);
}
function App() {
return (
<StoreProvider>
<AppContent />
</StoreProvider>
);
}
export default App;
Redux
import { configureStore, createSlice } from '@reduxjs/toolkit';
const userSlice = createSlice({
name: 'user',
initialState: null,
reducers: {
setUser: (state, action) => action.payload
}
});
const productsSlice = createSlice({
name: 'products',
initialState: [],
reducers: {
setProducts: (state, action) => action.payload
}
});
const cartSlice = createSlice({
name: 'cart',
initialState: [],
reducers: {
addToCart: (state, action) => [...state, action.payload],
removeFromCart: (state, action) => state.filter(item => item.id !== action.payload)
}
});
const loadingSlice = createSlice({
name: 'loading',
initialState: false,
reducers: {
setLoading: (state, action) => action.payload
}
});
const errorSlice = createSlice({
name: 'error',
initialState: null,
reducers: {
setError: (state, action) => action.payload
}
});
export const { setUser } = userSlice.actions;
export const { setProducts } = productsSlice.actions;
export const { addToCart, removeFromCart } = cartSlice.actions;
export const { setLoading } = loadingSlice.actions;
export const { setError } = errorSlice.actions;
const store = configureStore({
reducer: {
user: userSlice.reducer,
products: productsSlice.reducer,
cart: cartSlice.reducer,
loading: loadingSlice.reducer,
error: errorSlice.reducer
}
});
export default store;
import React, { useEffect } from 'react';
import { Provider, useDispatch, useSelector } from 'react-redux';
import store, { setUser, setProducts, addToCart, removeFromCart, setLoading, setError } from './store';
function AppContent() {
const dispatch = useDispatch();
const user = useSelector(state => state.user);
const products = useSelector(state => state.products);
const cart = useSelector(state => state.cart);
const loading = useSelector(state => state.loading);
const error = useSelector(state => state.error);
useEffect(() => {
async function fetchUser() {
dispatch(setLoading(true));
try {
const response = await fetch('/api/user');
const data = await response.json();
dispatch(setUser(data));
} catch (err) {
dispatch(setError('Failed to fetch user'));
} finally {
dispatch(setLoading(false));
}
}
fetchUser();
}, [dispatch]);
useEffect(() => {
async function fetchProducts() {
dispatch(setLoading(true));
try {
const response = await fetch('/api/products');
const data = await response.json();
dispatch(setProducts(data));
} catch (err) {
dispatch(setError('Failed to fetch products'));
} finally {
dispatch(setLoading(false));
}
}
fetchProducts();
}, [dispatch]);
return (
<div>
{loading && <div>Loading...</div>}
{error && <div>{error}</div>}
{user && <div>Welcome, {user.name}!</div>}
<h2>Products</h2>
<div className="products">
{products.map(product => (
<div key={product.id} className="product">
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => dispatch(addToCart(product))}>Add to Cart</button>
</div>
))}
</div>
<h2>Cart</h2>
<div className="cart">
{cart.map(item => (
<div key={item.id} className="cart-item">
{item.name}
{item.price}
dispatch(removeFromCart(item.id))}>Remove
))}
);
}
function App() {
return (
<Provider store={store}>
<AppContent />
</Provider>
);
}
export default App;
MobX
import { makeAutoObservable } from 'mobx';
class Store {
user = null;
products = [];
cart = [];
loading = false;
error = null;
constructor() {
makeAutoObservable(this);
}
setUser(user) { this.user = user; }
setProducts(products) { this.products = products; }
addToCart(product) { this.cart.push(product); }
removeFromCart(productId) { this.cart = this.cart.filter(item => item.id !== productId); }
setLoading(loading) { this.loading = loading; }
setError(error) { this.error = error; }
async fetchUser() {
this.setLoading(true);
try {
const response = await fetch('/api/user');
const data = await response.json();
this.setUser(data);
} catch (err) {
this.setError('Failed to fetch user');
} finally {
this.setLoading(false);
}
}
async fetchProducts() {
this.setLoading(true);
try {
const response = await fetch('/api/products');
const data = await response.json();
this.setProducts(data);
} catch (err) {
this.setError('Failed to fetch products');
} finally {
this.setLoading(false);
}
}
}
export const store = new Store();
import React, { useEffect } from 'react';
import { observer } from 'mobx-react-lite';
import { store } from './store';
const AppContent = observer(() => {
const { user, products, cart, loading, error } = store;
useEffect(() => {
store.fetchUser();
}, []);
useEffect(() => {
store.fetchProducts();
}, []);
return (
<div>
{loading && <div>Loading...</div>}
{error && <div>{error}</div>}
{user && <div>Welcome, {user.name}!</div>}
<h2>Products</h2>
<div className="products">
{products.map(product => (
<div key={product.id} className="product">
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => store.addToCart(product)}>Add to Cart</button>
</div>
))}
</div>
<h2>Cart</h2>
<div className="cart">
{cart.map(item => (
<div key={item.id} className="cart-item">
{item.name}
{item.price}
store.removeFromCart(item.id)}>Remove
))}
);
});
function App() {
return <AppContent />;
}
export default App;
Recoil
import { atom, selector } from 'recoil';
export const userAtom = atom({ key: 'user', default: null });
export const productsAtom = atom({ key: 'products', default: [] });
export const cartAtom = atom({ key: 'cart', default: [] });
export const loadingAtom = atom({ key: 'loading', default: false });
export const errorAtom = atom({ key: 'error', default: null });
export const cartTotalSelector = selector({
key: 'cartTotal',
get: ({ get }) => {
const cart = get(cartAtom);
return cart.reduce((total, item) => total + item.price, 0);
}
});
import React, { useEffect } from 'react';
import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
import {
userAtom, productsAtom, cartAtom, loadingAtom, errorAtom, cartTotalSelector
} from './store';
function AppContent() {
const [user, setUser] = useRecoilState(userAtom);
const [products, setProducts] = useRecoilState(productsAtom);
const [cart, setCart] = useRecoilState(cartAtom);
const [loading, setLoading] = useRecoilState(loadingAtom);
const [error, setError] = useRecoilState(errorAtom);
const cartTotal = useRecoilValue(cartTotalSelector);
useEffect(() => {
async function fetchUser() {
setLoading(true);
try {
const response = await fetch('/api/user');
const data = await response.json();
setUser(data);
} catch (err) {
setError('Failed to fetch user');
} finally {
setLoading(false);
}
}
fetchUser();
}, [setUser, setLoading, setError]);
useEffect(() => {
async function fetchProducts() {
setLoading(true);
try {
const response = await fetch('/api/products');
const data = await response.json();
setProducts(data);
} catch (err) {
setError('Failed to fetch products');
} finally {
setLoading(false);
}
}
fetchProducts();
}, [setProducts, setLoading, setError]);
const addToCart = (product) => {
setCart(prevCart => [...prevCart, product]);
};
const removeFromCart = (productId) => {
setCart(prevCart => prevCart.filter(item => item.id !== productId));
};
return (
<div>
{loading && <div>Loading...</div>}
{error && <div>{error}</div>}
{user && <div>Welcome, {user.name}!</div>}
<h2>Products</h2>
<div className="products">
{products.map(product => (
<div key={product.id} className="product">
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
))}
</div>
<h2>Cart</h2>
<div className="cart">
{cart.map(item => (
<div key={item.id} className="cart-item">
{item.name}
{item.price}
removeFromCart(item.id)}>Remove
))}
Total: {cartTotal}
);
}
function App() {
return (
<RecoilRoot>
<AppContent />
</RecoilRoot>
);
}
export default App;
Zustand
import create from 'zustand';
const useStore = create((set) => ({
user: null,
products: [],
cart: [],
loading: false,
error: null,
setUser: (user) => set({ user }),
setProducts: (products) => set({ products }),
addToCart: (product) => set((state) => ({ cart: [...state.cart, product] })),
removeFromCart: (productId) => set((state) => ({ cart: state.cart.filter(item => item.id !== productId) })),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
fetchUser: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/user');
const data = await response.json();
set({ user: data, loading: false });
} catch (err) {
set({ error: 'Failed to fetch user', loading: false });
}
},
fetchProducts: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/products');
const data = await response.json();
set({ products: data, loading: false });
} catch (err) {
set({ error: 'Failed to fetch products', loading: false });
}
}
}));
export default useStore;
import React, { useEffect } from 'react';
import useStore from './store';
function App() {
const {
user, products, cart, loading, error,
setUser, setProducts, addToCart, removeFromCart,
setLoading, setError, fetchUser, fetchProducts
} = useStore();
useEffect(() => {
fetchUser();
}, [fetchUser]);
useEffect(() => {
fetchProducts();
}, [fetchProducts]);
return (
<div>
{loading && <div>Loading...</div>}
{error && <div>{error}</div>}
{user && <div>Welcome, {user.name}!</div>}
<h2>Products</h2>
<div className="products">
{products.map(product => (
<div key={product.id} className="product">
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
))}
</div>
<h2>Cart</h2>
<div className="cart">
{cart.map(item => (
<div key={item.id} className="cart-item">
{item.name}
{item.price}
removeFromCart(item.id)}>Remove
))}
);
}
export default App;
Jotai
import { atom } from 'jotai';
export const userAtom = atom(null);
export const productsAtom = atom([]);
export const _cartAtom = atom([]);
export const cartAtom = atom(
(get) => get(_cartAtom),
(get, set, product) => set(_cartAtom, [...get(_cartAtom), product])
);
export const removeFromCartAtom = atom(
null,
(get, set, productId) => set(_cartAtom, get(_cartAtom).filter(item => item.id !== productId))
);
export const loadingAtom = atom(false);
export const errorAtom = atom(null);
export const cartTotalAtom = atom(
(get) => get(_cartAtom).reduce((total, item) => total + item.price, 0)
);
import React, { useEffect } from 'react';
import { useAtom } from 'jotai';
import {
userAtom, productsAtom, cartAtom, removeFromCartAtom,
loadingAtom, errorAtom, cartTotalAtom
} from './store';
function App() {
const [user, setUser] = useAtom(userAtom);
const [products, setProducts] = useAtom(productsAtom);
const [cart, addToCart] = useAtom(cartAtom);
const [, removeFromCart] = useAtom(removeFromCartAtom);
const [loading, setLoading] = useAtom(loadingAtom);
const [error, setError] = useAtom(errorAtom);
const [cartTotal] = useAtom(cartTotalAtom);
useEffect(() => {
async function fetchUser() {
setLoading(true);
try {
const response = await fetch('/api/user');
const data = await response.json();
setUser(data);
} catch (err) {
setError('Failed to fetch user');
} finally {
setLoading(false);
}
}
fetchUser();
}, [setUser, setLoading, setError]);
useEffect(() => {
async function fetchProducts() {
setLoading(true);
try {
const response = await fetch('/api/products');
const data = await response.json();
setProducts(data);
} catch (err) {
setError('Failed to fetch products');
} finally {
setLoading(false);
}
}
fetchProducts();
}, [setProducts, setLoading, setError]);
return (
<div>
{loading && <div>Loading...</div>}
{error && <div>{error}</div>}
{user && <div>Welcome, {user.name}!</div>}
<h2>Products</h2>
<div className="products">
{products.map(product => (
<div key={product.id} className="product">
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
))}
</div>
<h2>Cart</h2>
<div className="cart">
{cart.map(item => (
<div key={item.id} className="cart-item">
{item.name}
{item.price}
removeFromCart(item.id)}>Remove
))}
Total: {cartTotal}
);
}
export default App;
毒舌点评
状态管理确实很重要,但我见过太多开发者滥用状态管理库,导致应用变得更加复杂。
想象一下,当你为了一个简单的应用引入 Redux,结果写了大量的样板代码,这真的值得吗?
还有那些过度使用状态管理的开发者,把所有状态都放在全局状态中,结果导致状态更新时整个应用都重新渲染,影响性能。
所以,在选择状态管理方案时,一定要根据应用的复杂度来决定。对于小型应用,React Context + useReducer 可能就足够了;对于中型应用,可以考虑 Zustand 或 Jotai;对于大型应用,Redux 或 MobX 可能更合适。
当然,状态管理也不是万能的。良好的组件设计和代码组织同样重要。如果你的组件设计不合理,即使使用了最好的状态管理库,也无法解决根本问题。
最后,记住一句话:状态管理的目的是为了简化状态管理,而不是为了增加复杂度。如果你的状态管理方案让代码变得更复杂,那你就失败了。
<h4>
</h4>
<p>
</p>
<button onClick={() =>
</button>
</div>
</div>
</div>
<h4>
</h4>
<p>
</p>
<button onClick={() =>
</button>
</div>
</div>
</div>
<h4>
</h4>
<p>
</p>
<button onClick={() =>
</button>
</div>
</div>
</div>
<h4>
</h4>
<p>
</p>
<button onClick={() =>
</button>
</div>
<div className="cart-total">
<h3>
</h3>
</div>
</div>
</div>
<h4>
</h4>
<p>
</p>
<button onClick={() =>
</button>
</div>
</div>
</div>
<h4>
</h4>
<p>
</p>
<button onClick={() =>
</button>
</div>
<div className="cart-total">
<h3>
</h3>
</div>
</div>
</div>