Appearance
状态管理
状态管理是 React 应用开发中的重要环节,尤其是对于复杂的大型应用。本章节将介绍 React 中的状态管理方案。
11.1 为什么需要状态管理?
状态管理的必要性
- 组件间共享数据:多个组件需要访问和修改同一个数据
- 复杂的状态逻辑:状态更新依赖于多个因素,逻辑复杂
- 跨组件通信:避免 props 层层传递的问题
- 状态持久化:在页面刷新后保持状态
常见的状态管理场景
- 用户信息:登录状态、用户资料
- 应用配置:主题、语言设置
- 购物车:商品列表、数量
- 表单数据:多步骤表单的状态
- 全局通知:消息提示、错误信息
11.2 三种状态管理方案
方案1:useReducer + useContext
对于中小型应用,使用 useReducer 结合 useContext 是一个轻量级的状态管理方案。
基本使用
jsx
// src/contexts/StoreContext.jsx
import { createContext, useReducer, useContext } from 'react';
// 初始状态
const initialState = {
count: 0,
user: null,
todos: []
};
// Action 类型
const ActionTypes = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
SET_USER: 'SET_USER',
ADD_TODO: 'ADD_TODO',
REMOVE_TODO: 'REMOVE_TODO'
};
// Reducer 函数
function reducer(state, action) {
switch (action.type) {
case ActionTypes.INCREMENT:
return { ...state, count: state.count + 1 };
case ActionTypes.DECREMENT:
return { ...state, count: state.count - 1 };
case ActionTypes.SET_USER:
return { ...state, user: action.payload };
case ActionTypes.ADD_TODO:
return { ...state, todos: [...state.todos, action.payload] };
case ActionTypes.REMOVE_TODO:
return { ...state, todos: state.todos.filter((_, index) => index !== action.payload) };
default:
return state;
}
}
// 创建 Context
const StoreContext = createContext();
// 提供者组件
export function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StoreContext.Provider value={{ state, dispatch, ActionTypes }}>
{children}
</StoreContext.Provider>
);
}
// 自定义 Hook
export function useStore() {
const context = useContext(StoreContext);
if (!context) {
throw new Error('useStore must be used within a StoreProvider');
}
return context;
}使用全局状态
jsx
// App.jsx
import { StoreProvider } from './contexts/StoreContext';
import Counter from './components/Counter';
import UserProfile from './components/UserProfile';
import TodoList from './components/TodoList';
function App() {
return (
<StoreProvider>
<Counter />
<UserProfile />
<TodoList />
</StoreProvider>
);
}
// Counter.jsx
import { useStore } from '../contexts/StoreContext';
function Counter() {
const { state, dispatch, ActionTypes } = useStore();
return (
<div>
<p>计数:{state.count}</p>
<button onClick={() => dispatch({ type: ActionTypes.INCREMENT })}>增加</button>
<button onClick={() => dispatch({ type: ActionTypes.DECREMENT })}>减少</button>
</div>
);
}
// UserProfile.jsx
import { useStore } from '../contexts/StoreContext';
function UserProfile() {
const { state, dispatch, ActionTypes } = useStore();
function login() {
dispatch({
type: ActionTypes.SET_USER,
payload: { id: 1, name: '张三', email: 'zhangsan@example.com' }
});
}
function logout() {
dispatch({ type: ActionTypes.SET_USER, payload: null });
}
return (
<div>
{state.user ? (
<div>
<h2>{state.user.name}</h2>
<p>{state.user.email}</p>
<button onClick={logout}>退出登录</button>
</div>
) : (
<button onClick={login}>登录</button>
)}
</div>
);
}
// TodoList.jsx
import { useState } from 'react';
import { useStore } from '../contexts/StoreContext';
function TodoList() {
const { state, dispatch, ActionTypes } = useStore();
const [inputValue, setInputValue] = useState('');
function addTodo() {
if (inputValue) {
dispatch({ type: ActionTypes.ADD_TODO, payload: inputValue });
setInputValue('');
}
}
function removeTodo(index) {
dispatch({ type: ActionTypes.REMOVE_TODO, payload: index });
}
return (
<div>
<input
type="text"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
placeholder="请输入任务"
/>
<button onClick={addTodo}>添加</button>
<ul>
{state.todos.map((todo, index) => (
<li key={index}>
{todo}
<button onClick={() => removeTodo(index)}>删除</button>
</li>
))}
</ul>
</div>
);
}方案2:Redux Toolkit
Redux Toolkit (RTK) 是官方推荐的 Redux 工具集,它简化了传统 Redux 的使用。
安装
bash
npm install @reduxjs/toolkit react-redux基本配置
jsx
// src/store/index.js
import { configureStore, createSlice } from '@reduxjs/toolkit';
// 创建 counter slice
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
}
}
});
// 创建 user slice
const userSlice = createSlice({
name: 'user',
initialState: {
user: null
},
reducers: {
setUser: (state, action) => {
state.user = action.payload;
},
clearUser: (state) => {
state.user = null;
}
}
});
// 导出 actions
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const { setUser, clearUser } = userSlice.actions;
// 配置 store
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
user: userSlice.reducer
}
});
export default store;提供 store
jsx
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);使用 store
jsx
// Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from '../store';
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>计数:{count}</p>
<button onClick={() => dispatch(increment())}>增加</button>
<button onClick={() => dispatch(decrement())}>减少</button>
<button onClick={() => dispatch(incrementByAmount(5))}>增加 5</button>
</div>
);
}
// UserProfile.jsx
import { useSelector, useDispatch } from 'react-redux';
import { setUser, clearUser } from '../store';
function UserProfile() {
const user = useSelector(state => state.user.user);
const dispatch = useDispatch();
function login() {
dispatch(setUser({ id: 1, name: '张三', email: 'zhangsan@example.com' }));
}
function logout() {
dispatch(clearUser());
}
return (
<div>
{user ? (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={logout}>退出登录</button>
</div>
) : (
<button onClick={login}>登录</button>
)}
</div>
);
}方案3:Zustand
Zustand 是一个轻量级的状态管理库,它的 API 简单易用,适合中小型应用。
安装
bash
npm install zustand基本使用
jsx
// src/store/useStore.js
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
user: null,
todos: [],
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
removeTodo: (index) => set((state) => ({
todos: state.todos.filter((_, i) => i !== index)
}))
}));
export default useStore;使用 store
jsx
// Counter.jsx
import useStore from '../store/useStore';
function Counter() {
const count = useStore(state => state.count);
const increment = useStore(state => state.increment);
const decrement = useStore(state => state.decrement);
return (
<div>
<p>计数:{count}</p>
<button onClick={increment}>增加</button>
<button onClick={decrement}>减少</button>
</div>
);
}
// UserProfile.jsx
import useStore from '../store/useStore';
function UserProfile() {
const user = useStore(state => state.user);
const setUser = useStore(state => state.setUser);
const clearUser = useStore(state => state.clearUser);
function login() {
setUser({ id: 1, name: '张三', email: 'zhangsan@example.com' });
}
function logout() {
clearUser();
}
return (
<div>
{user ? (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={logout}>退出登录</button>
</div>
) : (
<button onClick={login}>登录</button>
)}
</div>
);
}
// TodoList.jsx
import { useState } from 'react';
import useStore from '../store/useStore';
function TodoList() {
const todos = useStore(state => state.todos);
const addTodo = useStore(state => state.addTodo);
const removeTodo = useStore(state => state.removeTodo);
const [inputValue, setInputValue] = useState('');
function handleAddTodo() {
if (inputValue) {
addTodo(inputValue);
setInputValue('');
}
}
return (
<div>
<input
type="text"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
placeholder="请输入任务"
/>
<button onClick={handleAddTodo}>添加</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo}
<button onClick={() => removeTodo(index)}>删除</button>
</li>
))}
</ul>
</div>
);
}11.3 状态持久化
使用 localStorage
jsx
// src/store/useStore.js
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
count: 0,
user: null,
todos: [],
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
removeTodo: (index) => set((state) => ({
todos: state.todos.filter((_, i) => i !== index)
}))
}),
{
name: 'my-app-storage' // localStorage 中的键名
}
)
);
export default useStore;Redux 持久化
bash
npm install redux-persistjsx
// src/store/index.js
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
// 配置
const persistConfig = {
key: 'root',
storage,
};
// 创建 slices
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
}
}
});
// 持久化 reducer
const persistedReducer = persistReducer(persistConfig, counterSlice.reducer);
// 配置 store
const store = configureStore({
reducer: {
counter: persistedReducer
}
});
// 创建 persistor
export const persistor = persistStore(store);
export const { increment, decrement } = counterSlice.actions;
export default store;jsx
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import store, { persistor } from './store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
</React.StrictMode>
);实战练习
练习1:使用 useReducer + useContext
jsx
// src/contexts/TodoContext.jsx
import { createContext, useReducer, useContext } from 'react';
const initialState = {
todos: [],
filter: 'all' // all, active, completed
};
const ActionTypes = {
ADD_TODO: 'ADD_TODO',
TOGGLE_TODO: 'TOGGLE_TODO',
REMOVE_TODO: 'REMOVE_TODO',
SET_FILTER: 'SET_FILTER'
};
function reducer(state, action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.payload, completed: false }]
};
case ActionTypes.TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
)
};
case ActionTypes.REMOVE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case ActionTypes.SET_FILTER:
return {
...state,
filter: action.payload
};
default:
return state;
}
}
const TodoContext = createContext();
export function TodoProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<TodoContext.Provider value={{ state, dispatch, ActionTypes }}>
{children}
</TodoContext.Provider>
);
}
export function useTodo() {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodo must be used within a TodoProvider');
}
return context;
}
// src/App.jsx
import { TodoProvider } from './contexts/TodoContext';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
import TodoFilter from './components/TodoFilter';
function App() {
return (
<TodoProvider>
<TodoForm />
<TodoList />
<TodoFilter />
</TodoProvider>
);
}
export default App;
// src/components/TodoForm.jsx
import { useState } from 'react';
import { useTodo } from '../contexts/TodoContext';
function TodoForm() {
const { dispatch, ActionTypes } = useTodo();
const [inputValue, setInputValue] = useState('');
function handleSubmit(e) {
e.preventDefault();
if (inputValue) {
dispatch({ type: ActionTypes.ADD_TODO, payload: inputValue });
setInputValue('');
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
placeholder="请输入任务"
/>
<button type="submit">添加</button>
</form>
);
}
export default TodoForm;
// src/components/TodoList.jsx
import { useTodo } from '../contexts/TodoContext';
function TodoList() {
const { state, dispatch, ActionTypes } = useTodo();
const { todos, filter } = state;
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
function toggleTodo(id) {
dispatch({ type: ActionTypes.TOGGLE_TODO, payload: id });
}
function removeTodo(id) {
dispatch({ type: ActionTypes.REMOVE_TODO, payload: id });
}
return (
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>删除</button>
</li>
))}
</ul>
);
}
export default TodoList;
// src/components/TodoFilter.jsx
import { useTodo } from '../contexts/TodoContext';
function TodoFilter() {
const { state, dispatch, ActionTypes } = useTodo();
const { filter } = state;
function setFilter(newFilter) {
dispatch({ type: ActionTypes.SET_FILTER, payload: newFilter });
}
return (
<div>
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
全部
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
未完成
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
已完成
</button>
</div>
);
}
export default TodoFilter;练习2:使用 Zustand
jsx
// src/store/useTodoStore.js
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useTodoStore = create(
persist(
(set) => ({
todos: [],
filter: 'all',
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }]
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
removeTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
})),
setFilter: (filter) => set({ filter })
}),
{
name: 'todo-storage'
}
)
);
export default useTodoStore;
// src/components/TodoForm.jsx
import { useState } from 'react';
import useTodoStore from '../store/useTodoStore';
function TodoForm() {
const addTodo = useTodoStore(state => state.addTodo);
const [inputValue, setInputValue] = useState('');
function handleSubmit(e) {
e.preventDefault();
if (inputValue) {
addTodo(inputValue);
setInputValue('');
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
placeholder="请输入任务"
/>
<button type="submit">添加</button>
</form>
);
}
export default TodoForm;
// src/components/TodoList.jsx
import useTodoStore from '../store/useTodoStore';
function TodoList() {
const todos = useTodoStore(state => state.todos);
const filter = useTodoStore(state => state.filter);
const toggleTodo = useTodoStore(state => state.toggleTodo);
const removeTodo = useTodoStore(state => state.removeTodo);
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return (
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>删除</button>
</li>
))}
</ul>
);
}
export default TodoList;
// src/components/TodoFilter.jsx
import useTodoStore from '../store/useTodoStore';
function TodoFilter() {
const filter = useTodoStore(state => state.filter);
const setFilter = useTodoStore(state => state.setFilter);
return (
<div>
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
全部
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
未完成
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
已完成
</button>
</div>
);
}
export default TodoFilter;通过本章节的学习,你已经掌握了 React 中的状态管理方案。在实际开发中,根据应用的规模和复杂度,选择合适的状态管理方案可以使代码更加清晰和易于维护。
