#你在 React 项目中是如何使用 Redux 的?
#一、Redux Toolkit — 现代标准
早期 Redux 需要手动定义 action types、action creators、reducer,每添加一个功能需要修改 3 个文件,冗余且易错。Redux Toolkit(RTK)是官方推荐的工具集,已是社区事实标准:
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment(state) {
state.value += 1;
},
decrement(state) {
state.value -= 1;
},
incrementBy(state, action) {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementBy } = counterSlice.actions;
export default counterSlice.reducer;一个 createSlice 调用同时生成了 reducer、action creators 和 action types。内部使用 Immer 处理不可变更新,可以"直接修改" state。
#二、项目结构(Feature-Based)
推荐按功能模块组织,而非按技术角色(actions/reducers/types)拆分:
src/
├── app/
│ ├── store.js # Store 配置
│ └── hooks.js # 类型安全的 hooks
├── features/
│ ├── auth/
│ │ ├── authSlice.js # Slice(reducer + actions)
│ │ ├── authApi.js # RTK Query API
│ │ ├── LoginForm.jsx
│ │ └── UserMenu.jsx
│ ├── todos/
│ │ ├── todosSlice.js
│ │ ├── TodoList.jsx
│ │ ├── TodoItem.jsx
│ │ └── AddTodo.jsx
│ └── cart/
│ ├── cartSlice.js
│ └── CartPage.jsx
└── index.jsx#Store 配置
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import authReducer from '../features/auth/authSlice';
import todosReducer from '../features/todos/todosSlice';
import cartReducer from '../features/cart/cartSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
todos: todosReducer,
cart: cartReducer,
},
});#自定义 Hooks
// app/hooks.js
import { useSelector, useDispatch } from 'react-redux';
export const useAppDispatch = () => useDispatch();
export const useAppSelector = useSelector;对于 TypeScript 项目,这一层是类型推断的关键:
// app/hooks.ts
import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();#三、react-redux Hooks
#useSelector — 读取状态
import { useAppSelector } from '../../app/hooks';
function TodoStats() {
const todos = useAppSelector(state => state.todos.list);
const total = todos.length;
const completed = todos.filter(t => t.done).length;
return (
<div>
<p>总计:{total},已完成:{completed}</p>
</div>
);
}useSelector 在每次 dispatch 后会重新执行选择器函数,如果返回值与上次不同(默认 === 比较)才触发组件重渲染。
对于复杂的派生数据,使用 createSelector(来自 reselect)做 memoized selector:
import { createSelector } from '@reduxjs/toolkit';
const selectTodos = (state) => state.todos.list;
const selectFilter = (state) => state.todos.filter;
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'completed':
return todos.filter(t => t.done);
case 'active':
return todos.filter(t => !t.done);
default:
return todos;
}
}
);
function FilteredTodoList() {
const filteredTodos = useAppSelector(selectFilteredTodos);
return <TodoList items={filteredTodos} />;
}#useDispatch — 触发 Action
import { useAppDispatch } from '../../app/hooks';
import { increment, decrement, incrementBy } from './counterSlice';
function Counter() {
const dispatch = useAppDispatch();
const value = useAppSelector(state => state.counter.value);
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
<button onClick={() => dispatch(incrementBy(10))}>+10</button>
</div>
);
}#四、异步操作:createAsyncThunk
// features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchTodos = createAsyncThunk(
'todos/fetchAll',
async (_, { rejectWithValue }) => {
try {
const res = await fetch('/api/todos');
if (!res.ok) throw new Error('请求失败');
return await res.json();
} catch (err) {
return rejectWithValue(err.message);
}
}
);
export const addTodo = createAsyncThunk(
'todos/add',
async (text, { rejectWithValue }) => {
try {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
return await res.json();
} catch (err) {
return rejectWithValue(err.message);
}
}
);
const todosSlice = createSlice({
name: 'todos',
initialState: {
list: [],
loading: false,
error: null,
filter: 'all',
},
reducers: {
setFilter(state, action) {
state.filter = action.payload;
},
toggleTodo(state, action) {
const todo = state.list.find(t => t.id === action.payload);
if (todo) todo.done = !todo.done;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = false;
state.list = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
.addCase(addTodo.fulfilled, (state, action) => {
state.list.push(action.payload);
});
},
});
export const { setFilter, toggleTodo } = todosSlice.actions;
export default todosSlice.reducer;在组件中使用:
function TodoPage() {
const dispatch = useAppDispatch();
const { list, loading, error } = useAppSelector(state => state.todos);
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return <TodoList items={list} />;
}#五、RTK Query — 服务端状态管理
RTK Query 是 RTK 内置的数据请求和缓存方案,类似于 React Query 但与 Redux 深度集成:
// features/api/apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Todo', 'User'],
endpoints: (builder) => ({
getTodos: builder.query({
query: () => '/todos',
providesTags: ['Todo'],
}),
addTodo: builder.mutation({
query: (newTodo) => ({
url: '/todos',
method: 'POST',
body: newTodo,
}),
invalidatesTags: ['Todo'],
}),
getUser: builder.query({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
}),
});
export const { useGetTodosQuery, useAddTodoMutation, useGetUserQuery } = apiSlice;组件中使用——自动管理 loading、error、缓存和重取:
function TodoList() {
const { data: todos, isLoading, error } = useGetTodosQuery();
const [addTodo, { isLoading: isAdding }] = useAddTodoMutation();
if (isLoading) return <Spinner />;
if (error) return <Error />;
return (
<div>
<button onClick={() => addTodo({ text: '新任务' })} disabled={isAdding}>
添加
</button>
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
</div>
);
}invalidatesTags 机制让 mutation 完成后自动重新获取相关数据,无需手动管理缓存失效。Store 中需注册 apiSlice.reducer 和 apiSlice.middleware。
#六、Provider 接入
// index.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
createRoot(document.getElementById('root')).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>
);#七、总结
现代 React 项目中使用 Redux 的标准实践:
- 用 Redux Toolkit,不要用原始 Redux API
- 用 createSlice 定义 reducer 和 actions
- 用 feature-based 目录结构组织代码
- 用 useSelector/useDispatch 在组件中交互
- 用 createAsyncThunk 处理异步,用 RTK Query 管理服务端缓存
- 用 createSelector 优化派生数据计算