Redux Toolkit 教程

项目概述

Redux Toolkit 是 Redux 的官方工具集,旨在简化 Redux 开发流程,减少样板代码,并提供一套最佳实践。它集成了 Immer 库用于不可变状态更新,内置了异步处理能力,并提供了 RTK Query 用于数据获取和缓存管理。Redux Toolkit 已经成为 Redux 官方推荐的标准方式,大幅降低了 Redux 的使用复杂度。

安装设置

1. 安装 Redux Toolkit

# 安装核心包
npm install @reduxjs/toolkit react-redux

# 或者使用 yarn
yarn add @reduxjs/toolkit react-redux

# 或者使用 pnpm
pnpm add @reduxjs/toolkit react-redux

2. 基本配置

创建 Redux Store

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {
    // 在这里添加你的 reducer
  }
})

提供 Redux Store

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './app/store'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
)

核心功能

1. 创建 Slice

Slice 是 Redux Toolkit 中的核心概念,它包含了 reducer 逻辑和 action 创建器。

// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  value: 0,
  status: 'idle'
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      // Redux Toolkit 允许我们在 reducers 中直接修改 state
      // 这是因为它使用了 Immer 库,会自动将这些修改转换为不可变更新
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

// 导出 action 创建器
export const { increment, decrement, incrementByAmount } = counterSlice.actions

// 导出 reducer
export default counterSlice.reducer

在 Store 中使用 Slice

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer
  }
})

在组件中使用

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, incrementByAmount } from './features/counter/counterSlice'

function Counter() {
  // 从 store 中获取状态
  const count = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
    </div>
  )
}

export default Counter

2. 异步逻辑处理

Redux Toolkit 内置了 createAsyncThunk 用于处理异步逻辑。

// src/features/users/usersSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { fetchUsers } from './usersAPI'

const initialState = {
  users: [],
  status: 'idle',
  error: null
}

// 创建异步 thunk
export const getUsers = createAsyncThunk(
  'users/fetchUsers',
  async () => {
    const response = await fetchUsers()
    return response.data
  }
)

export const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    // 同步 reducers
  },
  // 处理异步 action
  extraReducers: (builder) => {
    builder
      .addCase(getUsers.pending, (state) => {
        state.status = 'loading'
      })
      .addCase(getUsers.fulfilled, (state, action) => {
        state.status = 'succeeded'
        state.users = action.payload
      })
      .addCase(getUsers.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
  }
})

export default usersSlice.reducer

在组件中使用

import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { getUsers } from './features/users/usersSlice'

function UsersList() {
  const dispatch = useDispatch()
  const { users, status, error } = useSelector((state) => state.users)

  useEffect(() => {
    if (status === 'idle') {
      dispatch(getUsers())
    }
  }, [status, dispatch])

  let content

  if (status === 'loading') {
    content = <div>加载中...</div>
  } else if (status === 'succeeded') {
    content = (
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    )
  } else if (status === 'failed') {
    content = <div>错误: {error}</div>
  }

  return <div>{content}</div>
}

export default UsersList

3. RTK Query

RTK Query 是 Redux Toolkit 中的数据获取和缓存工具。

创建 API 服务

// src/services/api.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// 创建 API 服务
export const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts'
    }),
    getPostById: builder.query({
      query: (id) => `/posts/${id}`
    }),
    createPost: builder.mutation({
      query: (post) => ({
        url: '/posts',
        method: 'POST',
        body: post
      })
    })
  })
})

// 导出 hooks
export const { useGetPostsQuery, useGetPostByIdQuery, useCreatePostMutation } = api

在 Store 中配置

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import { api } from '../services/api'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    // 添加 API reducer
    [api.reducerPath]: api.reducer
  },
  // 添加 API 中间件
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware)
})

在组件中使用

import React from 'react'
import { useGetPostsQuery, useCreatePostMutation } from './services/api'

function PostsList() {
  // 使用查询 hook
  const { data: posts, isLoading, error } = useGetPostsQuery()
  const [createPost, { isLoading: isCreating }] = useCreatePostMutation()

  const handleCreatePost = async () => {
    await createPost({ title: '新文章', body: '文章内容' })
  }

  if (isLoading) return <div>加载中...</div>
  if (error) return <div>错误: {error}</div>

  return (
    <div>
      <button onClick={handleCreatePost} disabled={isCreating}>
        {isCreating ? '创建中...' : '创建文章'}
      </button>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

export default PostsList

4. 配置 Redux DevTools

Redux Toolkit 默认集成了 Redux DevTools,无需额外配置。

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'

// DevTools 已默认启用
export const store = configureStore({
  reducer: {
    // reducers
  }
})

// 可以通过 devTools 选项自定义
// export const store = configureStore({
//   reducer: {},
//   devTools: process.env.NODE_ENV !== 'production'
// })

5. 不可变更新

Redux Toolkit 使用 Immer 库,允许我们在 reducers 中直接修改状态,而 Immer 会自动将这些修改转换为不可变更新。

// src/features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  todos: []
}

export const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTodo: (state, action) => {
      // 直接修改 state
      state.todos.push(action.payload)
    },
    toggleTodo: (state, action) => {
      // 直接修改嵌套对象
      const todo = state.todos.find(todo => todo.id === action.payload)
      if (todo) {
        todo.completed = !todo.completed
      }
    },
    updateTodoText: (state, action) => {
      const { id, text } = action.payload
      const todo = state.todos.find(todo => todo.id === id)
      if (todo) {
        todo.text = text
      }
    }
  }
})

export const { addTodo, toggleTodo, updateTodoText } = todosSlice.actions
export default todosSlice.reducer

高级功能

1. 自定义 Middleware

// src/app/middleware.js
const loggerMiddleware = (store) => (next) => (action) => {
  console.log('Dispatching:', action)
  const result = next(action)
  console.log('New state:', store.getState())
  return result
}

export default loggerMiddleware

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import loggerMiddleware from './middleware'

export const store = configureStore({
  reducer: {
    // reducers
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(loggerMiddleware)
})

2. 组合 Reducers

// src/features/index.js
import { combineReducers } from '@reduxjs/toolkit'
import counterReducer from './counter/counterSlice'
import todosReducer from './todos/todosSlice'
import usersReducer from './users/usersSlice'

const rootReducer = combineReducers({
  counter: counterReducer,
  todos: todosReducer,
  users: usersReducer
})

export default rootReducer

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from '../features'

export const store = configureStore({
  reducer: rootReducer
})

3. 持久化状态

# 安装持久化库
npm install redux-persist
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // 默认使用 localStorage
import counterReducer from '../features/counter/counterSlice'

// 持久化配置
const persistConfig = {
  key: 'root',
  storage
}

// 创建持久化 reducer
const persistedReducer = persistReducer(persistConfig, counterReducer)

export const store = configureStore({
  reducer: {
    counter: persistedReducer
  }
})

// 创建持久化 store
export const persistor = persistStore(store)
// 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 './app/store'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>
)

4. 代码分割

// src/features/largeFeature/largeFeatureSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  data: []
}

export const largeFeatureSlice = createSlice({
  name: 'largeFeature',
  initialState,
  reducers: {
    // reducers
  }
})

export const { actions } = largeFeatureSlice
export default largeFeatureSlice.reducer

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

const store = configureStore({
  reducer: {
    counter: counterReducer
    // 大型特性的 reducer 将通过代码分割动态添加
  }
})

// 动态添加 reducer 的方法
export const injectReducer = (key, reducer) => {
  if (store.asyncReducers && store.asyncReducers[key]) {
    return
  }
  
  if (!store.asyncReducers) {
    store.asyncReducers = {}
  }
  
  store.asyncReducers[key] = reducer
  store.replaceReducer({
    ...store.getState(),
    [key]: reducer
  })
}

export default store

实际应用场景

1. 计数器应用

// src/features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  value: 0,
  step: 1
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += state.step
    },
    decrement: (state) => {
      state.value -= state.step
    },
    setStep: (state, action) => {
      state.step = action.payload
    },
    reset: (state) => {
      state.value = 0
    }
  }
})

export const { increment, decrement, setStep, reset } = counterSlice.actions
export default counterSlice.reducer
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, setStep, reset } from './features/counter/counterSlice'

function Counter() {
  const { value, step } = useSelector((state) => state.counter)
  const dispatch = useDispatch()
  const [newStep, setNewStep] = useState(step)

  const handleSetStep = () => {
    dispatch(setStep(Number(newStep)))
  }

  return (
    <div>
      <h1>计数器: {value}</h1>
      <div>
        <label>步长: </label>
        <input
          type="number"
          value={newStep}
          onChange={(e) => setNewStep(e.target.value)}
        />
        <button onClick={handleSetStep}>设置步长</button>
      </div>
      <div>
        <button onClick={() => dispatch(increment())}>+{step}</button>
        <button onClick={() => dispatch(decrement())}>-{step}</button>
        <button onClick={() => dispatch(reset())}>重置</button>
      </div>
    </div>
  )
}

export default Counter

2. 待办事项应用

// src/features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { v4 as uuidv4 } from 'uuid'

const initialState = {
  todos: []
}

export const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTodo: (state, action) => {
      state.todos.push({
        id: uuidv4(),
        text: action.payload,
        completed: false
      })
    },
    toggleTodo: (state, action) => {
      const todo = state.todos.find(todo => todo.id === action.payload)
      if (todo) {
        todo.completed = !todo.completed
      }
    },
    removeTodo: (state, action) => {
      state.todos = state.todos.filter(todo => todo.id !== action.payload)
    },
    updateTodo: (state, action) => {
      const { id, text } = action.payload
      const todo = state.todos.find(todo => todo.id === id)
      if (todo) {
        todo.text = text
      }
    }
  }
})

export const { addTodo, toggleTodo, removeTodo, updateTodo } = todosSlice.actions
export default todosSlice.reducer
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { addTodo, toggleTodo, removeTodo, updateTodo } from './features/todos/todosSlice'

function TodoList() {
  const todos = useSelector((state) => state.todos.todos)
  const dispatch = useDispatch()
  const [inputText, setInputText] = useState('')

  const handleAddTodo = () => {
    if (inputText.trim()) {
      dispatch(addTodo(inputText))
      setInputText('')
    }
  }

  return (
    <div>
      <h1>待办事项</h1>
      <div>
        <input
          type="text"
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
          placeholder="添加新待办事项"
        />
        <button onClick={handleAddTodo}>添加</button>
      </div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            <span onClick={() => dispatch(toggleTodo(todo.id))}>{todo.text}</span>
            <button onClick={() => dispatch(removeTodo(todo.id))}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

export default TodoList

3. 异步数据获取

// src/features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// 模拟 API 调用
const fetchPosts = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts')
  return response.json()
}

const fetchPostById = async (id) => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
  return response.json()
}

// 异步 thunks
export const getPosts = createAsyncThunk('posts/fetchPosts', fetchPosts)
export const getPostById = createAsyncThunk('posts/fetchPostById', fetchPostById)

const initialState = {
  posts: [],
  currentPost: null,
  status: 'idle',
  error: null
}

export const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      // 获取所有帖子
      .addCase(getPosts.pending, (state) => {
        state.status = 'loading'
      })
      .addCase(getPosts.fulfilled, (state, action) => {
        state.status = 'succeeded'
        state.posts = action.payload
      })
      .addCase(getPosts.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
      // 获取单个帖子
      .addCase(getPostById.pending, (state) => {
        state.status = 'loading'
      })
      .addCase(getPostById.fulfilled, (state, action) => {
        state.status = 'succeeded'
        state.currentPost = action.payload
      })
      .addCase(getPostById.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
  }
})

export default postsSlice.reducer
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { getPosts, getPostById } from './features/posts/postsSlice'

function PostsApp() {
  const dispatch = useDispatch()
  const { posts, currentPost, status, error } = useSelector((state) => state.posts)

  useEffect(() => {
    dispatch(getPosts())
  }, [dispatch])

  const handleViewPost = (id) => {
    dispatch(getPostById(id))
  }

  return (
    <div>
      <h1>帖子列表</h1>
      {status === 'loading' && <div>加载中...</div>}
      {status === 'failed' && <div>错误: {error}</div>}
      {status === 'succeeded' && (
        <div>
          <div>
            <h2>所有帖子</h2>
            <ul>
              {posts.slice(0, 10).map((post) => (
                <li key={post.id}>
                  <h3>{post.title}</h3>
                  <button onClick={() => handleViewPost(post.id)}>查看详情</button>
                </li>
              ))}
            </ul>
          </div>
          {currentPost && (
            <div>
              <h2>帖子详情</h2>
              <h3>{currentPost.title}</h3>
              <p>{currentPost.body}</p>
            </div>
          )}
        </div>
      )}
    </div>
  )
}

export default PostsApp

代码优化建议

  1. 文件结构组织

    • 按功能模块组织文件,每个功能模块包含自己的 slice、组件和 API
    • 使用 features 目录存放功能模块,app 目录存放全局配置
  2. 使用 createSelector

    • 对于复杂的状态选择逻辑,使用 createSelector 进行记忆化,提高性能
    import { createSelector } from '@reduxjs/toolkit'
    
    const selectTodos = (state) => state.todos.todos
    
    export const selectCompletedTodos = createSelector(
      [selectTodos],
      (todos) => todos.filter(todo => todo.completed)
    )
  3. 合理使用 RTK Query

    • 对于数据获取场景,优先使用 RTK Query 而非手动处理异步逻辑
    • 利用 RTK Query 的缓存机制减少不必要的网络请求
  4. 避免过度使用 Redux

    • 对于组件内部状态,使用 React 的 useState 和 useReducer
    • 对于跨组件共享状态,使用 Redux
  5. 优化 reducer 逻辑

    • 保持 reducer 逻辑简单纯粹
    • 对于复杂业务逻辑,考虑使用自定义 middleware
  6. 使用 TypeScript

    • 为 Redux state、actions 和 selectors 添加类型定义
    • 提高代码可维护性和类型安全性
  7. 测试

    • 为 reducers 和 actions 编写单元测试
    • 测试异步 thunks 的各种状态
  8. 性能优化

    • 使用 useSelector 的依赖数组优化重渲染
    • 对于大型应用,考虑使用 createSelector 和代码分割

参考资源

« 上一篇 React Router DOM 教程 - React Router 的 DOM 绑定 下一篇 » Express.js 教程