你在 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.reducerapiSlice.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 的标准实践:

  1. 用 Redux Toolkit,不要用原始 Redux API
  2. 用 createSlice 定义 reducer 和 actions
  3. 用 feature-based 目录结构组织代码
  4. 用 useSelector/useDispatch 在组件中交互
  5. 用 createAsyncThunk 处理异步,用 RTK Query 管理服务端缓存
  6. 用 createSelector 优化派生数据计算