React 状态管理 Redux
概述
Redux 是一个可预测的状态容器,用于管理应用的全局状态。本章将学习 Redux 的核心概念、与 React 的集成,以及现代 Redux Toolkit 的使用。
🏪 Redux 核心概念
基础 Redux 设置
jsx
// 模拟 Redux 功能
function createStore(reducer, initialState) {
let state = initialState;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
};
return { getState, dispatch, subscribe };
}
// Action Types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
const SET_COUNT = 'SET_COUNT';
// Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const reset = () => ({ type: RESET });
const setCount = (count) => ({ type: SET_COUNT, payload: count });
// Reducer
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 };
case DECREMENT:
return { ...state, count: state.count - 1 };
case RESET:
return { ...state, count: 0 };
case SET_COUNT:
return { ...state, count: action.payload };
default:
return state;
}
}
// React Redux 连接
const ReduxContext = React.createContext();
function Provider({ store, children }) {
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
React.useEffect(() => {
const unsubscribe = store.subscribe(forceUpdate);
return unsubscribe;
}, [store]);
return (
<ReduxContext.Provider value={store}>
{children}
</ReduxContext.Provider>
);
}
function useSelector(selector) {
const store = React.useContext(ReduxContext);
return selector(store.getState());
}
function useDispatch() {
const store = React.useContext(ReduxContext);
return store.dispatch;
}
// 计数器组件
function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>Redux 计数器</h2>
<div style={{ fontSize: '48px', margin: '20px 0', color: '#007bff' }}>
{count}
</div>
<div>
<button
onClick={() => dispatch(decrement())}
style={{ margin: '0 10px', padding: '10px 20px' }}
>
-1
</button>
<button
onClick={() => dispatch(increment())}
style={{ margin: '0 10px', padding: '10px 20px' }}
>
+1
</button>
<button
onClick={() => dispatch(reset())}
style={{ margin: '0 10px', padding: '10px 20px' }}
>
重置
</button>
</div>
</div>
);
}
function BasicReduxExample() {
const store = React.useMemo(() => createStore(counterReducer), []);
return (
<Provider store={store}>
<Counter />
</Provider>
);
}复杂状态管理
jsx
// 待办事项 Actions
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
const SET_FILTER = 'SET_FILTER';
const addTodo = (text) => ({
type: ADD_TODO,
payload: { id: Date.now(), text, completed: false }
});
const toggleTodo = (id) => ({
type: TOGGLE_TODO,
payload: id
});
const deleteTodo = (id) => ({
type: DELETE_TODO,
payload: id
});
const setFilter = (filter) => ({
type: SET_FILTER,
payload: filter
});
// 待办事项 Reducer
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, action.payload];
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case DELETE_TODO:
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
// 过滤器 Reducer
function filterReducer(state = 'ALL', action) {
switch (action.type) {
case SET_FILTER:
return action.payload;
default:
return state;
}
}
// 根 Reducer
function rootReducer(state = {}, action) {
return {
todos: todosReducer(state.todos, action),
filter: filterReducer(state.filter, action)
};
}
// 待办事项组件
function TodoApp() {
const todos = useSelector(state => state.todos || []);
const filter = useSelector(state => state.filter || 'ALL');
const dispatch = useDispatch();
const [inputValue, setInputValue] = React.useState('');
const filteredTodos = todos.filter(todo => {
switch (filter) {
case 'ACTIVE':
return !todo.completed;
case 'COMPLETED':
return todo.completed;
default:
return true;
}
});
const handleAddTodo = (e) => {
e.preventDefault();
if (inputValue.trim()) {
dispatch(addTodo(inputValue.trim()));
setInputValue('');
}
};
const todoStats = {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length
};
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>Redux 待办事项</h2>
{/* 添加待办事项 */}
<form onSubmit={handleAddTodo} style={{ marginBottom: '20px' }}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="添加新的待办事项..."
style={{
padding: '10px',
width: '70%',
border: '1px solid #ddd',
borderRadius: '4px 0 0 4px'
}}
/>
<button
type="submit"
style={{
padding: '10px 20px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '0 4px 4px 0'
}}
>
添加
</button>
</form>
{/* 过滤器 */}
<div style={{ marginBottom: '20px' }}>
{['ALL', 'ACTIVE', 'COMPLETED'].map(filterType => (
<button
key={filterType}
onClick={() => dispatch(setFilter(filterType))}
style={{
margin: '0 5px',
padding: '8px 16px',
backgroundColor: filter === filterType ? '#007bff' : '#f8f9fa',
color: filter === filterType ? 'white' : '#333',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
{filterType === 'ALL' ? '全部' : filterType === 'ACTIVE' ? '进行中' : '已完成'}
</button>
))}
</div>
{/* 统计信息 */}
<div style={{
backgroundColor: '#f8f9fa',
padding: '15px',
borderRadius: '8px',
marginBottom: '20px'
}}>
<div>总计: {todoStats.total} | 进行中: {todoStats.active} | 已完成: {todoStats.completed}</div>
</div>
{/* 待办事项列表 */}
<div>
{filteredTodos.length === 0 ? (
<div style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
{filter === 'ALL' ? '暂无待办事项' : '没有符合条件的待办事项'}
</div>
) : (
filteredTodos.map(todo => (
<div
key={todo.id}
style={{
display: 'flex',
alignItems: 'center',
padding: '12px',
margin: '8px 0',
backgroundColor: 'white',
border: '1px solid #ddd',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
style={{ marginRight: '12px' }}
/>
<span
style={{
flex: 1,
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#6c757d' : '#333'
}}
>
{todo.text}
</span>
<button
onClick={() => dispatch(deleteTodo(todo.id))}
style={{
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
padding: '6px 12px',
borderRadius: '4px',
cursor: 'pointer'
}}
>
删除
</button>
</div>
))
)}
</div>
</div>
);
}
function ComplexReduxExample() {
const store = React.useMemo(() => createStore(rootReducer), []);
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}🛠️ Redux Toolkit 现代方法
Slice 和现代 Redux
jsx
// 模拟 Redux Toolkit createSlice
function createSlice({ name, initialState, reducers }) {
const actionTypes = {};
const actionCreators = {};
Object.keys(reducers).forEach(key => {
const type = `${name}/${key}`;
actionTypes[key] = type;
actionCreators[key] = (payload) => ({ type, payload });
});
const reducer = (state = initialState, action) => {
const reducerKey = Object.keys(actionTypes).find(
key => actionTypes[key] === action.type
);
if (reducerKey && reducers[reducerKey]) {
return reducers[reducerKey](state, action);
}
return state;
};
return {
name,
reducer,
actions: actionCreators
};
}
// 用户 Slice
const userSlice = createSlice({
name: 'user',
initialState: {
profile: null,
loading: false,
error: null
},
reducers: {
setLoading: (state, action) => ({
...state,
loading: action.payload
}),
setProfile: (state, action) => ({
...state,
profile: action.payload,
loading: false,
error: null
}),
setError: (state, action) => ({
...state,
error: action.payload,
loading: false
}),
clearUser: (state) => ({
...state,
profile: null,
error: null
})
}
});
// 购物车 Slice
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
total: 0
},
reducers: {
addItem: (state, action) => {
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
return state;
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
return state;
},
updateQuantity: (state, action) => {
const item = state.items.find(item => item.id === action.payload.id);
if (item) {
item.quantity = action.payload.quantity;
if (item.quantity <= 0) {
state.items = state.items.filter(i => i.id !== item.id);
}
}
state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
return state;
},
clearCart: (state) => {
state.items = [];
state.total = 0;
return state;
}
}
});
// 组合 Reducer
function combineReducers(slices) {
return (state = {}, action) => {
const newState = {};
Object.keys(slices).forEach(key => {
newState[key] = slices[key].reducer(state[key], action);
});
return newState;
};
}
const rootReducerToolkit = combineReducers({
user: userSlice,
cart: cartSlice
});
// 电商应用组件
function ECommerceApp() {
const user = useSelector(state => state.user);
const cart = useSelector(state => state.cart);
const dispatch = useDispatch();
const products = [
{ id: 1, name: 'iPhone 15', price: 5999 },
{ id: 2, name: 'MacBook Pro', price: 12999 },
{ id: 3, name: 'AirPods Pro', price: 1899 }
];
const handleLogin = () => {
dispatch(userSlice.actions.setLoading(true));
setTimeout(() => {
dispatch(userSlice.actions.setProfile({
id: 1,
name: '张三',
email: 'zhangsan@example.com'
}));
}, 1000);
};
const handleLogout = () => {
dispatch(userSlice.actions.clearUser());
dispatch(cartSlice.actions.clearCart());
};
return (
<div style={{ padding: '20px' }}>
<header style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px',
paddingBottom: '20px',
borderBottom: '1px solid #ddd'
}}>
<h1>Redux Toolkit 电商演示</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
{user.profile ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span>欢迎, {user.profile.name}</span>
<button onClick={handleLogout}>登出</button>
</div>
) : (
<button onClick={handleLogin} disabled={user.loading}>
{user.loading ? '登录中...' : '登录'}
</button>
)}
<div style={{
backgroundColor: '#007bff',
color: 'white',
padding: '8px 12px',
borderRadius: '20px'
}}>
购物车: {cart.items.length} 件商品 ¥{cart.total}
</div>
</div>
</header>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '30px' }}>
{/* 商品列表 */}
<div>
<h2>商品列表</h2>
<div style={{ display: 'grid', gap: '15px' }}>
{products.map(product => (
<div key={product.id} style={{
padding: '20px',
border: '1px solid #ddd',
borderRadius: '8px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<h3 style={{ margin: '0 0 5px 0' }}>{product.name}</h3>
<p style={{ margin: 0, color: '#007bff', fontSize: '18px', fontWeight: 'bold' }}>
¥{product.price}
</p>
</div>
<button
onClick={() => dispatch(cartSlice.actions.addItem(product))}
style={{
backgroundColor: '#28a745',
color: 'white',
border: 'none',
padding: '10px 20px',
borderRadius: '6px'
}}
>
加入购物车
</button>
</div>
))}
</div>
</div>
{/* 购物车 */}
<div>
<h2>购物车</h2>
{cart.items.length === 0 ? (
<div style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
购物车为空
</div>
) : (
<div>
{cart.items.map(item => (
<div key={item.id} style={{
padding: '15px',
border: '1px solid #ddd',
borderRadius: '8px',
marginBottom: '10px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h4 style={{ margin: '0 0 5px 0' }}>{item.name}</h4>
<p style={{ margin: 0, color: '#666' }}>¥{item.price} x {item.quantity}</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<button
onClick={() => dispatch(cartSlice.actions.updateQuantity({
id: item.id,
quantity: item.quantity - 1
}))}
style={{ padding: '5px 10px' }}
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() => dispatch(cartSlice.actions.updateQuantity({
id: item.id,
quantity: item.quantity + 1
}))}
style={{ padding: '5px 10px' }}
>
+
</button>
<button
onClick={() => dispatch(cartSlice.actions.removeItem(item.id))}
style={{ color: 'red', background: 'none', border: 'none' }}
>
删除
</button>
</div>
</div>
</div>
))}
<div style={{
padding: '15px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
textAlign: 'center',
fontSize: '18px',
fontWeight: 'bold'
}}>
总计: ¥{cart.total}
</div>
<button
onClick={() => dispatch(cartSlice.actions.clearCart())}
style={{
width: '100%',
padding: '15px',
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '8px',
marginTop: '10px',
fontSize: '16px'
}}
>
清空购物车
</button>
</div>
)}
</div>
</div>
</div>
);
}
function ReduxToolkitExample() {
const store = React.useMemo(() => createStore(rootReducerToolkit), []);
return (
<Provider store={store}>
<ECommerceApp />
</Provider>
);
}📝 本章小结
通过本章学习,你应该掌握了:
Redux 核心概念
- ✅ Store、Action、Reducer 的作用
- ✅ 单向数据流和状态不可变性
- ✅ React-Redux 的连接方式
- ✅ Redux Toolkit 的现代用法
最佳实践
- 状态结构设计:保持状态扁平化和规范化
- Action 设计:使用描述性的 Action 类型
- Reducer 纯函数:避免副作用和状态突变
- 性能优化:使用 selector 优化和 memo
- 开发工具:利用 Redux DevTools 调试
继续学习:下一章 - React 项目构建与部署