对于 Vue 开发者来说,React 的状态管理可能是最需要转变思维方式的部分之一。本文将从 Vue 开发者熟悉的角度出发,详细介绍 React 的状态管理方案,并通过实战示例帮助你快速掌握。
本地状态管理对比
Vue 的响应式系统
在 Vue 中,我们习惯使用 data
选项来定义组件的本地状态:
vue
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++ // 直接修改状态
}
}
}
</script>
React 的 useState
而在 React 中,我们使用 useState
Hook 来管理状态:
jsx
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // 使用 setter 函数更新状态
};
return (
<div>
<p>{count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
主要区别:
- Vue 的状态是响应式的,可以直接修改
- React 的状态是不可变的,必须通过 setter 函数更新
- React 的状态更新是异步的,多个更新会被批处理
复杂状态管理
使用 useReducer
当组件状态逻辑较复杂时,可以使用 useReducer
来管理状态:
jsx
import React, { useReducer } from 'react';
// 定义 reducer 函数
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, {
id: Date.now(),
text: action.payload,
completed: false
}];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case 'REMOVE_TODO':
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [input, setInput] = useState('');
const handleAdd = () => {
if (!input.trim()) return;
dispatch({ type: 'ADD_TODO', payload: input });
setInput('');
};
return (
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
/>
<button onClick={handleAdd}>添加</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({
type: 'TOGGLE_TODO',
payload: todo.id
})}
/>
<span>{todo.text}</span>
<button onClick={() => dispatch({
type: 'REMOVE_TODO',
payload: todo.id
})}>
删除
</button>
</li>
))}
</ul>
</div>
);
}
这种模式类似于 Vuex 的 mutations,但更加轻量和灵活。
全局状态管理
Context API
React 的 Context API 类似于 Vue 的 provide/inject:
jsx
// ThemeContext.js
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// App.js
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
// Layout.js
function Layout() {
const { theme, toggleTheme } = useTheme();
return (
<div className={`app ${theme}`}>
<button onClick={toggleTheme}>
切换主题
</button>
<Content />
</div>
);
}
状态管理库对比
- Vuex vs Redux
Vuex:
js
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
})
Redux:
js
// reducer.js
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
default:
return state;
}
}
// actions.js
const increment = () => ({ type: 'INCREMENT' });
const incrementAsync = () => dispatch => {
setTimeout(() => {
dispatch(increment());
}, 1000);
};
// store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
counterReducer,
applyMiddleware(thunk)
);
- Pinia vs Zustand
Pinia:
js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++;
}
}
});
Zustand:
js
import create from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}));
实战示例:购物车
让我们通过一个购物车示例来实践状态管理:
jsx
// types.ts
interface Product {
id: number;
name: string;
price: number;
}
interface CartItem extends Product {
quantity: number;
}
// cartStore.js
import create from 'zustand';
const useCartStore = create((set, get) => ({
items: [],
totalAmount: 0,
addToCart: (product) => set(state => {
const existingItem = state.items.find(item => item.id === product.id);
if (existingItem) {
return {
items: state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
totalAmount: state.totalAmount + product.price
};
}
return {
items: [...state.items, { ...product, quantity: 1 }],
totalAmount: state.totalAmount + product.price
};
}),
removeFromCart: (productId) => set(state => {
const item = state.items.find(item => item.id === productId);
if (!item) return state;
return {
items: state.items.filter(item => item.id !== productId),
totalAmount: state.totalAmount - (item.price * item.quantity)
};
}),
updateQuantity: (productId, quantity) => set(state => {
const item = state.items.find(item => item.id === productId);
if (!item) return state;
const quantityDiff = quantity - item.quantity;
return {
items: state.items.map(item =>
item.id === productId
? { ...item, quantity }
: item
),
totalAmount: state.totalAmount + (item.price * quantityDiff)
};
})
}));
// ProductList.jsx
function ProductList() {
const [products] = useState([
{ id: 1, name: '商品1', price: 100 },
{ id: 2, name: '商品2', price: 200 },
{ id: 3, name: '商品3', price: 300 }
]);
const addToCart = useCartStore(state => state.addToCart);
return (
<div className="product-list">
{products.map(product => (
<div key={product.id} className="product-item">
<h3>{product.name}</h3>
<p>¥{product.price}</p>
<button onClick={() => addToCart(product)}>
加入购物车
</button>
</div>
))}
</div>
);
}
// Cart.jsx
function Cart() {
const { items, totalAmount, updateQuantity, removeFromCart } = useCartStore();
return (
<div className="cart">
<h2>购物车</h2>
{items.map(item => (
<div key={item.id} className="cart-item">
<span>{item.name}</span>
<input
type="number"
min="1"
value={item.quantity}
onChange={e => updateQuantity(item.id, +e.target.value)}
/>
<span>¥{item.price * item.quantity}</span>
<button onClick={() => removeFromCart(item.id)}>
删除
</button>
</div>
))}
<div className="cart-total">
总计:¥{totalAmount}
</div>
</div>
);
}
性能优化
-
状态分割
jsx// 不好的做法 const [state, setState] = useState({ user: null, posts: [], comments: [] });
// 好的做法 const [user, setUser] = useState(null); const [posts, setPosts] = useState([]); const [comments, setComments] = useState([]);
2. **使用 useMemo 缓存计算结果**
```jsx
const totalPrice = useMemo(() => {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
}, [items]);
-
使用 useCallback 缓存函数
jsxconst handleUpdate = useCallback((id, value) => { updateQuantity(id, value); }, [updateQuantity]);
-
避免不必要的重渲染
jsx// CartItem.jsx const CartItem = memo(function CartItem({ item, onUpdate, onRemove }) { return ( <div className="cart-item"> <span>{item.name}</span> <input type="number" value={item.quantity} onChange={e => onUpdate(item.id, +e.target.value)} /> <button onClick={() => onRemove(item.id)}>删除</button> </div> ); });
调试技巧
- 使用 React DevTools
- 查看组件树
- 检查状态变化
- 分析重渲染原因
-
使用 Redux DevTools
jsimport { devtools } from 'zustand/middleware';
const useStore = create( devtools( (set) => ({ // store implementation }) ) );
3. **使用日志中间件**
```js
const useStore = create((set) => {
const originalSet = set;
set = (...args) => {
console.log('prev state:', get());
console.log('action:', args[0]);
originalSet(...args);
console.log('next state:', get());
};
return {
// store implementation
};
});
最佳实践
- 状态设计原则
- 保持状态最小化
- 避免冗余数据
- 合理拆分状态
- 遵循单一数据源
- 更新模式
- 使用不可变更新
- 批量处理更新
- 避免深层嵌套
- 性能考虑
- 合理使用缓存
- 避免过度订阅
- 及时清理副作用
小结
-
React 状态管理的特点:
- 不可变性
- 单向数据流
- 函数式更新
- 异步批处理
-
从 Vue 到 React 的转变:
- 告别直接修改
- 拥抱函数式
- 重视性能优化
- 合理使用 Hooks
-
开发建议:
- 从简单开始
- 循序渐进
- 注重实践
- 保持好奇
下一篇文章,我们将深入探讨 React 的组件设计模式,帮助你更好地组织和复用代码。
如果觉得这篇文章对你有帮助,别忘了点个赞 👍