Redux 官网文档笔记

196 阅读45分钟

一、Redux 概念与数据流

官方原文:Redux 中文官网

1. State 管理

让我们从一个小的 React 计数器组件开始。 它跟踪组件状态中的数字,并在单击按钮时增加数字:

function Counter() {
  // State: counter 值
  const [counter, setCounter] = useState(0)

  // Action: 当事件发生后,触发状态更新的代码
  const increment = () => {
    setCounter(prevCounter => prevCounter + 1)
  }

  // View: 视图定义
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  )
}

这是一个包含以下部分的自包含应用程序:

  • state:驱动应用的真实数据源头
  • view:基于当前状态的视图声明性描述
  • actions:根据用户输入在应用程序中发生的事件,并触发状态更新

接下来简要介绍 "单向数据流(one-way data flow)":

  • 用 state 来描述应用程序在特定时间点的状况
  • 基于 state 来渲染出 View
  • 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新,生成新的 state
  • 基于新的 state 重新渲染 View

image.png

然而,当我们有多个组件需要共享和使用相同 state时,可能会变得很复杂,尤其是当这些组件位于应用程序的不同部分时。有时这可以通过 **"提升 state"**到父组件来解决,但这并不总是有效。

解决这个问题的一种方法是从组件中提取共享 state,并将其放入组件树之外的一个集中位置。这样,我们的组件树就变成了一个大“view”,任何组件都可以访问 state 或触发 action,无论它们在树中的哪个位置!

通过定义和分离 state 管理中涉及的概念并强制执行维护 view 和 state 之间独立性的规则,代码变得更结构化和易于维护。

这就是 Redux 背后的基本思想:应用中使用集中式的全局状态来管理,并明确更新状态的模式,以便让代码具有可预测性。

2. 术语

2.1 Action

action 是一个具有 type 字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件.

type 字段是一个字符串,给这个 action 一个描述性的名字,比如"todos/todoAdded"。我们通常把那个类型的字符串写成**“域/事件名称”**,其中第一部分是这个 action 所属的特征或类别,第二部分是发生的具体事情。

action 对象可以有其他字段,其中包含有关发生的事情的附加信息。按照惯例,我们将该信息放在名为 payload 的字段中。

一个典型的 action 对象可能如下所示:

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}

2.2 Action Creator

action creator 是一个创建并返回一个 action 对象的函数。它的作用是让你不必每次都手动编写 action 对象:

const addTodo = text => {
  return {
    type: 'todos/todoAdded',
    payload: text
  }
}

2.3 Reducer

reducer 是一个函数,接收当前的 state 和一个 action 对象,必要时决定如何更新状态,并返回新状态。函数签名是:(state, action) => newState你可以将 reducer 视为一个事件监听器,它根据接收到的 action(事件)类型处理事件。

Reducer 必需符合以下规则:

  • 仅使用 stateaction 参数计算新的状态值
  • 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新(immutable updates)
  • 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码

Reducer 函数内部的逻辑通常遵循以下步骤:

  • 检查 reducer 是否关心这个 action
    • 如果是,则复制 state,使用新值更新 state 副本,然后返回新 state
  • 否则,返回原来的 state 不变

下面是 reducer 的小例子,展示了每个 reducer 应该遵循的步骤:

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // 检查 reducer 是否关心这个 action
  if (action.type === 'counter/increment') {
    // 如果是,复制 `state`
    return {
      ...state,
      // 使用新值更新 state 副本
      value: state.value + 1
    }
  }
  // 返回原来的 state 不变
  return state
}

Reducer 可以在内部使用任何类型的逻辑来决定新状态应该是什么,如 if/elseswitch、循环等等。

2.4 Reducers 规则拓展

我们之前说过 reducer 必须始终遵循一些特殊规则

  • 应该只根据 stateaction 参数计算新的状态值
  • 不允许修改现有的 state。相反,必须通过复制现有值并对复制的值进行更改来进行 immutable updates 。
  • 不能做任何异步逻辑或包含"副作用"

image.png

任何遵循这些规则的函数也被称为 " 纯 " 函数(普通函数)。

但为什么这些规则很重要?有几个不同的原因:

  • Redux 的目标之一是让你的代码可预测。如果仅根据输入参数计算函数的输出,则更容易理解该代码的工作原理并对其进行测试。
  • 另一方面,如果一个函数依赖于自身外部的变量,或者行为随机,你永远不知道运行它时会发生什么。
  • 如果一个函数修改了其他值,包括它的参数,这可能会改变应用程序的工作方式。这可能是常见的错误来源,例如 “ 我更新了状态,但现在我的 UI 没有在应该更新的时候更新!”
  • 一些 Redux DevTools 功能依赖于让你的 reducer 正确地遵循这些规则

2.5 Reducers 和 Immutable Updates

早些时候,我们谈到了 " mutation " (修改现有的对象/数组值) 和 " immutability " (无法更改).

在 Redux 中,我们的 reducer 永远不允许改变原始/当前状态值!

// ❌ 非法 - 默认情况下,这会改变状态!
state.value = 123

在 Redux 中不能改变状态有几个原因:

  • 会导致错误,例如 UI 无法正确更新显示最新值
  • 这使得更难理解为什么以及如何更新状态
  • 使编写测试变得更加困难
  • 破坏了正确使用“时间旅行调试”的能力
  • 违背了 Redux 的预期精神和使用模式

如果不能改变原始 state ,那么如何返回一个更新的 state 呢?

//Reducers 只能 复制 原始值,并只能改变这些副本。
// ✅ 做了复制,所以是安全的
return {
  ...state,
  value: 123
}

我们可以手动编写 immutable updates,通过使用 JavaScript 的数组/对象扩展运算符和其他返回原始值副本的函数。

当数据嵌套时,这变得更加困难。Immutable Updates 的一个关键规则是,你必须复制需要更新的每一层嵌套

但是,如果你认为“以这种方式手动编写 immutable updates 看起来很难记住和正确执行” ......啊对对对!:)

手工编写不可变的更新逻辑很困难,并且在 reducer 中意外改变状态是 Redux 用户最常犯的一个错误

2.6 Store

当前 Redux 应用的 state 存在于一个名为 store 的对象中。

store 是通过传入一个 reducer 来创建的,并且有一个名为 getState 的方法,可以获取到当前的状态值:

//redux
import { creatStore } from 'redux'
const store = creatStore({ reducer: counterReducer })
console.log(store.getState()) // {value: 0}


//@reduxjs/toolkit
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState()) // {value: 0}

2.7 Dispatch

Redux store 有一个方法叫 dispatch更新 state 的唯一方法是调用 store.dispatch() 并传入一个 action 对象。 store 将执行所有 reducer 函数并计算出更新后的 state,调用 getState() 可以获取新 state。

store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1} 触发了一次

dispatch 一个 action 可以形象的理解为 "触发一个事件"。发生了一些事情,我们希望 store 知道这件事。 Reducer 就像事件监听器一样,当它们收到关注的 action 后,它就会更新 state 作为响应。

我们通常调用 action creator 来调用 action:

const increment = () => {
  return {
    type: 'counter/increment'
  }
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2} 触发了两次

2.8 Selector

Selector 函数可以从 store 状态树中提取指定的片段。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 可以避免重复这样的读取逻辑:

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

3. Redux 数据流

早些时候,我们谈到了“单向数据流”,它描述了更新应用程序的以下步骤序列:

  • State 描述了应用程序在特定时间点的状况
  • 基于 state 来渲染视图
  • 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新
  • 基于新的 state 重新渲染视图

具体来说,对于 Redux,我们可以将这些步骤分解为更详细的内容:

  • 初始启动:
    • 使用最顶层的 root reducer 函数创建 Redux store
    • store 调用一次 root reducer,并将返回值保存为它的初始 state
    • 当视图 首次渲染时,视图组件访问 Redux store 的当前 state,并使用该数据来决定要呈现的内容。同时监听 store 的更新,以便他们可以知道 state 是否已更改。
  • 更新环节:
    • 应用程序中发生了某些事情,例如用户单击按钮
    • dispatch 一个 action 到 Redux store,例如 dispatch({type: 'counter/increment'})
    • store 用之前的 state 和当前的 action 再次运行 reducer 函数,并将返回值保存为新的 state
    • store 通知所有订阅过的视图,通知它们 store 发生更新
    • 每个订阅过 store 数据的视图 组件都会检查它们需要的 state 部分是否被更新。
    • 发现数据被更新的每个组件都强制使用新数据重新渲染,紧接着更新网页

动画的方式来表达数据流更新:

ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif

4. 总结

  • Redux 是一个管理全局应用状态的库
    • Redux 通常与 React-Redux 库一起使用,把 Redux 和 React 集成在一起
    • Redux Toolkit 是编写 Redux 逻辑的推荐方式
  • Redux 使用 "单向数据流"
    • State 描述了应用程序在某个时间点的状态,视图基于该 state 渲染
    • 当应用程序中发生某些事情时:
      • 视图 dispatch 一个 action
      • store 调用 reducer,随后根据发生的事情来更新 state
      • store 将 state 发生了变化的情况通知 UI
    • 视图基于新 state 重新渲染
  • Redux 有这几种类型的代码
    • Action 是有 type 字段的纯对象,描述发生了什么
    • Reducer 是纯函数,基于先前的 state 和 action 来计算新的 state
    • 每当 dispatch 一个 action 后,store 就会调用 root reducer

5. 基础使用实践

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app">
      <section>
        <span id="counter">0</span>
        <button id="increment">+</button>
        <button id="decrement">-</button>
      </section>
    </div>
    <script type="module" src="/main.js"></script>
  </body>
</html>

import { legacy_createStore as createStore } from 'redux'

//DOM
const counter = document.querySelector('#counter')
const increment = document.querySelector('#increment')
const decrement = document.querySelector('#decrement')

//初始state
const initState = {
  value: 0
}

//Reducer
const reducer = (state = initState, action) => {
  switch (action.type) {
    case 'couter/increment':
      return { ...state, value: state.value + 1 }
    case 'couter/decrement':
      return { ...state, value: state.value - 1 }
    default:
      return state
  }
}

//Store
const store = createStore(reducer)

//Render
const render = () => {
  counter.innerHTML = store.getState().value
}
render()

//Subscribe: 动态订阅render
store.subscribe(render)

//Event'
increment.addEventListener('click', ()=> {
  //点击派发事件
  store.dispatch({type: 'couter/increment'})
})
decrement.addEventListener('click', ()=> {
  //点击派发事件
  store.dispatch({type: 'couter/decrement'})
})

二、Reducer(根Reducer)

官方文档:Redux 深入浅出, 第三节: State, Actions, 和 Reducers | Redux 中文官网

1. Reducer切片

Redux reducer 通常根据更新的 Redux state 部分进行拆分。例如我们的应用 state 当前有两个顶级部分:state.todosstate.filters。因此,可以将大的根 reducer 函数拆分为两个较小的 reducer - todos ReducerfiltersReducer

Redux 应用程序状态的特定部分的 redux 应用 state 称为“ slice reducer ”。通常,一些 actions 对象将与特定的 slice reducer 密切相关,因此 action type 字符串应该以该功能的名称(像 'todos')开头,并描述发生的事件 (像 'todoAdded'), 连接在一起成为一个字符串 ('todos/todoAdded')。

在项目中,新建一个 features 文件夹,然后在里面创建一个 todos 文件夹。新建一个名为 todosSlice.js 的文件,然后在该文件中编写对todos的业务代码

// src/features/todos/todosSlice.js
const initialState = [
  { id: 0, text: 'Learn React', completed: true },
  { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
  { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
]

function nextTodoId(todos) {
  const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
  return maxId + 1
}

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    //处理不同的逻辑
    case 'tasks/taskAdded': {
      // !!要对副本进行操作
      return [
        ...state,
        {
          id: nextTodoId(state),
          text: action.payload,
          completed: false
        }
      ]
    //other...
    default:
      return state
  }
}

filtersReducer也是如此

// src/features/filters/filtersSlice.js
const initialState = {
  status: 'All',
  colors: []
}

export default function filtersReducer(state = initialState, action) {
  switch (action.type) {
    case 'filters/statusFilterChanged': {
      return {
        // 同样,要复制的嵌套层次少了一层
        ...state,
        status: action.payload
      }
    }
    default:
      return state
  }
}

2. RootReducer

现在有两个独立的 slice 文件,每个文件都有自己的 slice reducer 函数。但是,Redux 存储在创建时需要 一个 根 reducer 函数。那么,如何才能在不将所有代码放在一个大函数中的情况下重新使用根 redux 呢?

由于 reducers 是一个普通的 JS 函数,我们可以将 slice reducer 重新导入 reducer.js,并编写一个新的根 reducer,它唯一的工作就是调用其他两个函数。

// src/reducer.js
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

export default function rootReducer(state = {}, action) {
  // 返回一个新的根 state 对象
  return {
    // `state.todos` 的值是 todos reducer 返回的值
    todos: todosReducer(state.todos, action),
    // 对于这两个reducer,我们只传入它们的状态 slice
    filters: filtersReducer(state.filters, action)
  }
}

这些 reducer 中的每一个都在管理自己的全局状态部分。每个 reducer 的 state 参数都是不同的,并且对应于它管理的 state 部分。

3. combineReducers合并Reducer

新的根 reducer 对每个 slice 都做同样的事情:调用 slice reducer,传入属于该 reducer 的 state slice,并将结果分配回根 state 对象。如果要添加更多 slice ,则重复该模式。

Redux 核心库包含一个名为 combineReducers 的实用程序,它为我们执行相同的样板步骤。我们可以用 combineReducers 生成的较短的 rootReducer 替换手写的 rootReducer

安装

npm install redux

使用

// src/reducer.js
import { combineReducers } from 'redux'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const rootReducer = combineReducers({
  // 定义一个名为`todos`的顶级状态字段,由`todosReducer`处理
  todos: todosReducer,
  filters: filtersReducer
})

export default rootReducer

4. 总结

  • Redux 应用程序使用普通的 JS 对象、数组和 primitives 作为状态值
    • 根状态值应该是一个普通的 JS 对象
    • 状态应该包含使应用程序工作所需的最少数据量
    • 类、Promises、函数和其他非普通值不应进入 Redux 状态
    • redux 不得创建随机值,例如 Math.random()Date.now()
    • 可以将其他不在 Redux 存储中的状态值(如本地组件状态)与 Redux 并排放置
  • actions 是带有 type 并且描述发生了什么的普通对象
    • type 字段应该是一个可读的字符串,通常写成 'feature/eventName'
    • 动作可能包含其他值,这些值通常存储在 action.payload 字段中
    • 操作应该包含描述发生的事情所需的最少数据量
  • Reducers 是看起来像的函数 (state, action) => newState
    • Reducers 必须始终遵循特殊规则:
      • 仅根据 stateaction 参数计算新状态
      • 永远不要改变现有的 state - 总是返回一个副本
      • 不要有像 AJAX 调用或异步逻辑这样的“副作用”
  • Reducers 应该被拆分以使它们更易于阅读
    • Reducer 通常根据顶级状态键或状态“ slices ”进行拆分
    • Reducers 通常写在“ slice ”文件中,组织成“ feature ”文件夹
    • Reducers 可以与 Redux combineReducers 函数结合使用
    • 用于 combineReducers 定义顶级状态对象键的键名

5. 实践代码

src/features/filters/filtersSlice.js

//filtersReducer的state
export const StatusFilters = {
  All: 'all',
  Active: 'active',
  Completed: 'completed'
}

const initialState = {
  status: StatusFilters.All,
  colors: []
}

//处理逻辑
export default function filtersReducer(state = initialState, action) {
  switch (action.type) {
    case 'filters/statusFilterChanged': {
      return {
        //少了一层要复制的嵌套
        ...state,
        status: action.payload
      }
    }
    case 'filters/colorFilterChanged': {
      let { color, changeType } = action.payload
      const { colors } = state

      switch (changeType) {
        case 'added': {
          if (colors.includes(color)) {
            // 设置过的颜色。不做改变状态
            return state
          }

          return {
            ...state,
            colors: state.colors.concat(color),
          }
        }
        case 'removed': {
          return {
            ...state,
            colors: state.colors.filter(
              (existingColor) => existingColor !== color
            ),
          }
        }
        default:
          return state
      }
    }
    default:
      return state
  }
}

src/features/todos/todosSlice.js

const initialState = [
  { id: 0, text: 'Learn React', completed: true },
  { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
  { id: 2, text: 'Build something fun!', completed: false, color: 'blue' },
]

function nextTodoId(todos) {
  const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
  return maxId + 1
}

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    case 'todos/todoAdded': {
      // 可以返回只是新的todos数组-没有额外的对象围绕它
      return [
        ...state,
        {
          id: nextTodoId(state),
          text: action.payload,
          completed: false,
        },
      ]
    }
    case 'todos/todoToggled': {
      return state.map((todo) => {
        if (todo.id !== action.payload) {
          return todo
        }

        return {
          ...todo,
          completed: !todo.completed,
        }
      })
    }
    case 'todos/colorSelected': {
      const { color, todoId } = action.payload
      return state.map((todo) => {
        if (todo.id !== todoId) {
          return todo
        }

        return {
          ...todo,
          color,
        }
      })
    }
    case 'todos/todoDeleted': {
      return state.filter((todo) => todo.id !== action.payload)
    }
    case 'todos/allCompleted': {
      return state.map((todo) => {
        return { ...todo, completed: true }
      })
    }
    case 'todos/completedCleared': {
      return state.filter((todo) => !todo.completed)
    }
    default:
      return state
  }
}

src/reducer

//使用combineReducers统一暴露管理多个reducer slice
import { combineReducers } from 'redux'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const rootReducer = combineReducers({
  // 定义一个名为`todos`的顶级状态字段,由`todosReducer`处理
  todos: todosReducer,
  filters: filtersReducer
})

export default rootReducer

三、Store(中心仓库)

1. Redux Store

Redux store 汇集了构成应用程序的 state、actions 和 reducers。store 有以下几个职责:

重要的是要注意 Redux 应用程序中只有一个 store。当你想要拆分数据处理逻辑时,你将使用 reducer composition 并创建多个可以组合在一起reducer,而不是创建单独的 store。

1.1 创建 Store

每个 Redux store 都有一个根 reducer 函数。在上一节中,我们使用 combineReducers 创建了一个根 reducer 函数。在我们的示例应用程序中,该根 reducer 当前在 src/reducer.js 中定义。让我们导入根 reducer 并创建第一个 store。

Redux 核心库有一个**createStore** API 可以创建 store。新建一个名为 store.js 的文件,并导入 createStore 和根 reducer。然后,调用 createStore 并传入根 reducer :

// src/store.js
import { createStore } from 'redux'
import rootReducer from './reducer'

const store = createStore(rootReducer)

export default store

1.2 加载初始 State(持久化)

createStore 也可以接受 preloadedState 值作为其第二个参数。你可以使用它在创建 store 时添加初始数据,例如包含在从服务器接收到的 HTML 页面中的值,或保存在 localStorage 中并在用户再次访问该页面时读回的值,如下所示:

import { createStore } from 'redux'
import rootReducer from './reducer'

let preloadedState
const persistedTodosString = localStorage.getItem('todos')

if (persistedTodosString) {
  preloadedState = {
    todos: JSON.parse(persistedTodosString)
  }
}

const store = createStore(rootReducer, preloadedState)

2. Redux Store 内部

查看 Redux Store 内部实现对于学习 store 有所帮助。这是一个关于 Redux Store 实现的简化示例,大约 25 行代码:

function createStore(reducer, preloadedState) {
  let state = preloadedState
  const listeners = []

  function getState() {
    return state
  }

  function subscribe(listener) {
    listeners.push(listener)
    return function unsubscribe() {
      const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    state = reducer(state, action)
    listeners.forEach(listener => listener())
  }

  dispatch({ type: '@@redux/INIT' })

  return { dispatch, subscribe, getState }
}

这个小版本的 Redux store 运行良好,你可以使用这个自己编写的 createStore 函数替换实际的 Redux createStore 函数(实际的 Redux 存储实现更长,更复杂,但是其中大部分是评论信息、警告信息和一些极端情况的处理)。

如你所见,这里的实际逻辑相当短:

  • store 内部有当前的 state 值和 reducer 函数
  • getState 返回当前 state 值
  • subscribe 保存一个监听回调数组并返回一个函数来移除新的回调
  • dispatch 调用 reducer,保存 state,并运行监听器
  • store 在启动时 dispatch 一个 action 来初始化 reducers 的 state
  • store API 是一个对象,里面有 {dispatch, subscribe, getState}

特别强调其中之一:注意 getState 只返回当前的 state 值。这意味着默认情况下,没有什么可以阻止你意外改变当前 state 值! 此代码将运行没有任何错误,但它是不正确的:

const state = store.getState()
// ❌ 不要这样做 - 它改变了当前 state!
state.filters.status = 'Active'

换句话说:

  • 当你调用 getState() 时,Redux store 不会产生 state 值的额外副本。它与根 reducer 函数返回的引用完全相同。
  • Redux store 对于意外更改没有做任何防护,我们可以在 reducer 内部或者 store 外部改变状态,所以必须小心避免意外更改。

无意中发生变动的一个常见原因是对数组进行排序。调用 array.sort() 实际上会改变现有数组。如果我们调用 const sortedTodos = state.todos.sort(),我们最终会无意中改变真实的 store 状态。

3. 使用 Enhancers 创建 Store

我们的项目在 src/exampleAddons/enhancers.js 文件中有两个小示例 store enhancers 可用:

  • sayHiOnDispatch: enhancer,每次 dispatched 一个 action 时总是将'Hi'!记录到控制台
  • includeMeaningOfLife: enhancer 总是将字段 meaningOfLife: 42 添加到 getState() 返回值中
// src/exampleAddons/enhancers.js

//实际是重写了一遍createStore,然后做一些自己需要添加的一些操作,
//再把 自定义的操作与新的store 一起返回
export const sayHiOnDispatch = (createStore) => {
  return (rootReducer, preloadedState, enhancers) => {
    const store = createStore(rootReducer, preloadedState, enhancers)

    //为每次action执行一些自定的操作
    function newDispatch(action) {
      const result = store.dispatch(action)
      //自定义操作:log一下
      console.log('Hi!')
      return result
    }

    return { ...store, dispatch: newDispatch }
  }
}

export const includeMeaningOfLife = (createStore) => {
  return (rootReducer, preloadedState, enhancers) => {
    const store = createStore(rootReducer, preloadedState, enhancers)

    //为每次getState执行一些自定的操作
    function newGetState() {
      //自定义操作:返回一个新的state,多一个meaningOfLife属性
      return {
        ...store.getState(),
        meaningOfLife: 42,
      }
    }

    return { ...store, getState: newGetState }
  }
}

为createState添加enhancer:

// src/store.js
import { createStore, compose } from 'redux'
import rootReducer from './reducer'
import {
  sayHiOnDispatch,
  includeMeaningOfLife
} from './exampleAddons/enhancers'

//① 添加单个enhancer
const store = createStore(rootReducer, undefined, sayHiOnDispatch)
//① 添加多个enhancer
const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife)
const store2 = createStore(rootReducer, undefined, composedEnhancer)
//没有preloadedState时可不用忽略preload传值
//const store2 = createStore(rootReducer, composedEnhancer)

export default store

使用store时发生的变化

// src/index.js

//传了多个enhancer
import store from './store'

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: 'Hi!'

console.log('State after dispatch: ', store.getState())
// log: {todos: [...], filters: {status, colors}, meaningOfLife: 42}

输出记录:

image.png

解释:

sayHiOnDispatch enhancer 用自己的专用版本 dispatch 包装了原始的 store.dispatch 函数。 当我们调用 store.dispatch() 时,我们实际上是从 sayHiOnDispatch 调用包装函数,它调用原始函数然后打印 'Hi'。

因此,我们可以看到两个 enhancer 同时修改了 store 的行为。sayHiOnDispatch 改变了 dispatch 的工作方式,而 includeMeaningOfLife 改变了 getState 的工作方式。

4. Middleware(中间件)

Enhancers 非常强大,因为其可以覆盖或替换 store 的任何方法:dispatchgetStatesubscribe

但是,很多时候,我们只需要自定义 dispatch 的行为方式。 如果有一种方法可以在 dispatch 运行时添加一些自定义行为,那就太好了。

Redux 使用一种称为 middleware 的特殊插件来让我们自定义 dispatch 函数。

Redux middleware 在 dispatch action 和到达 reducer 之间提供第三方扩展点。 人们使用 Redux middleware 进行日志记录、崩溃报告、异步 API 通信、路由等。

4.1 使用 Middleware

你可以使用 store enhancers 自定义 Redux store。Redux Middleware 实际上是在 Redux 内置的一个非常特殊的 store enhancer 之上实现的,称为 applyMiddleware

由于我们已经知道如何将 enhancers 添加到 store,现在应该能够做到这一点。我们将从 applyMiddleware 本身开始,将添加三个已包含在此项目中的示例 middleware。

// src/exampleAddons/middleware.js
export const print1 = (storeAPI) => (next) => (action) => {
  console.log('1')
  return next(action)
}

export const print2 = (storeAPI) => (next) => (action) => {
  console.log('2')
  return next(action)
}

export const print3 = (storeAPI) => (next) => (action) => {
  console.log('3')
  return next(action)
}
// src/store.js
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'

const middlewareEnhancer = applyMiddleware(print1, print2, print3)

// 将 enhancer 为第二参数,因为没有 preloadedState
const store = createStore(rootReducer, middlewareEnhancer)

export default store

正如他们的名字一样,当 dispatch 一个 action 时,这些 middleware 中的每一个都会打印一个数字。

// src/index.js
import store from './store'

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: '1'
// log: '2'
// log: '3'

工作原理:

Middleware 围绕 store 的 dispatch 方法形成管线。当我们调用 store.dispatch(action) 时,实际上 调用了管线中的第一个 Middleware。 然后,该 Middleware 可以在看到该操作时做任何它想做的事情。通常,Middleware 会检查 action 是否是它关心的特定 type,就像 reducer 一样。如果它是匹配到的 type,Middleware 可能会运行一些自定义逻辑。否则,它将 dispatch 传递给管线中的下一个 Middleware。

不像 reducer,middleware 内部可能有副作用,包括超时和其他异步逻辑。

在这种情况下,action 通过:

  1. print1 middleware(我们将其视为 store.dispatch
  2. print2 middleware
  3. print3 middleware
  4. 原来的 store.dispatch
  5. store 中的根 reducer

而且由于这些都是函数调用,它们都从该调用堆栈中 返回。因此,print1 middleware 是第一个运行的,也是最后一个完成的。

4.2 自定义Middlewave

Redux middleware 被编写为一系列的三个嵌套函数。让我们看看这种模式是什么样子的。我们将首先尝试使用 function 关键字编写这个 middleware,以便更清楚发生了什么

// 使用 ES5 function 来编写 Middleware

// 外层 function:
function exampleMiddleware(storeAPI) {
  return function wrapDispatch(next) {
    return function handleAction(action) {
      // 在这里做任何事情:用 next(action) 向前传递 action,
      // 或者使用 storeAPI.dispatch(action) 重启管线
      // 这里也可以使用 storeAPI.getState()

      return next(action)
    }
  }
}

让我们分解这三个函数的作用以及它们的参数是什么。

  • exampleMiddleware:外层函数其实就是 “middleware” 本身。它将被 applyMiddleware 调用,并接收包含 store 的 {dispatch, getState} 函数的 storeAPI 对象。这些是相同的 dispatchgetState 函数,它们实际上是 store 的一部分。如果你调用这个 dispatch 函数,它会将 action 发送到 middleware 管线的 start。这只会被调用一次。
  • wrapDispatch:中间函数接收一个名为 next 的函数作为其参数。这个函数实际上是管线中的 next middleware。如果这个 middleware 是序列中的最后一个,那么 next 实际上是原始的 store.dispatch 函数。调用 next(action) 会将 action 传递给管线中的 next middleware。这也只调用一次
  • handleAction:最后,内部函数接收当前的 action 作为其参数,并在 每次 dispatch action 时调用。

因为这些是普通函数,我们也可以使用 ES6 箭头函数来编写它们。这让我们可以把它们写得更短,因为箭头函数不必有一个 return 语句,但如果你还不熟悉箭头函数和隐式返回,它也可能会有点难以阅读。

这是与上面相同的示例,使用箭头函数:

const anotherExampleMiddleware = storeAPI => next => action => {
  // 当每个 action 都被 dispatch 时,在这里做一些事情

  return next(action)
}

4.3 拦截action自定义返回值

const alwaysReturnHelloMiddleware = storeAPI => next => action {
  const originalResult = next(action);
  // 忽略原始结果,返回其他内容
  return 'Hello!'
}

const middlewareEnhancer = applyMiddleware(alwaysReturnHelloMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)

const dispatchResult = store.dispatch({type: 'some/action'})
console.log(dispatchResult)
// log: 'Hello!'

4.4 拦截action添加异步任务

Middleware 通常会寻找一个特定的 action,然后在该 action 被 dispatch 时做一些事情。Middleware 也有能力在里面运行异步逻辑。我们可以编写一个 middleware,当它匹配到某个 action 时,它会延迟打印一些东西:

const delayedMessageMiddleware = storeAPI => next => action => {
  if (action.type === 'todos/todoAdded') {
    setTimeout(() => {
      console.log('Added a new todo: ', action.payload)
    }, 1000)
  }
  return next(action)
}

该 middleware 将寻找 “todo added” 的 action。每次匹配到一个,它都会设置一个 1 秒的计时器,然后将 action 的有效负载打印到控制台。

4.5 Middleware 用例

所以我们可以用中间件做很多事!

当一个 middleware 遇到 dispatch 一个 action 时,它可以做到任何想做的事:

  • 将某些内容记录到控制台
  • 设置定时
  • 进行异步 API 调用
  • 修改 action
  • 暂停 action,甚至完全停止

以及你能想到的任何其他事情。

特别的是,middleware 旨在 包含具有副作用的逻辑。此外,middleware 可以修改 dispatch 来接受 不是 普通 action 对象的东西。

5. Redux DevTools(浏览器拓展插件)

最后,配置 store 还有一件非常重要的事情。

Redux 专门设计用于更容易理解你的 state 何时、何地、为何以及如何随时间变化。作为其中的一部分,Redux 的构建是为了支持使用一个插件 Redux DevTools,它向你显示 dispatch 了哪些 action 的历史记录,这些操作包含什么,以及在每个 dispatch action 之后 state 如何变化。

Redux DevTools UI 可作为 Chrome 和 [Firefox](addons.mozilla.org/en-US/firef…

安装后,打开浏览器的 DevTools 窗口。你现在应该在那里看到一个新的 “Redux” 选项卡。它还没有做任何事情 —— 必须先设置其与 Redux store 连通。

5.1 将 DevTools 添加到 Store(使用多个中间件)

安装扩展后,我们需要配置 store,以便 DevTools 可以看到里面发生了什么。DevTools 需要添加特定的 store enhancer 才能实现这一点。

Redux DevTools Extension docs 有一些关于如何设置 store 的说明,只是列出的步骤有点复杂。但是,有一个名为 “redux-devtools-extension” 的 NPM 包可以处理复杂的部分。该包导出了一个专门的 composeWithDevTools 函数,我们可以使用它来代替原始的 Redux compose 函数。

看起来是这样的:

// src/store.js
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'

const composedEnhancer = composeWithDevTools(
  // 示例:在此处添加你实际要使用的任何 middleware(代替compose)
  applyMiddleware(print1, print2, print3)
  // 其他 store enhancers(如果有)
)

const store = createStore(rootReducer, composedEnhancer)
export default store

确保 index.js 在导入 store 后仍在 dispatch action。现在,在浏览器的 DevTools 窗口中打开 Redux DevTools 选项卡。你应该会看到如下所示的内容:

image.png

左侧有一个已 dispatch 的 action 列表。如果我们单击其中一个,右窗格会显示几个选项卡:

  • 该操作对象的内容 (The contents of that action object)
  • 在 reducer 运行后,整个 Redux 状态 (The entire Redux state as it looked after the reducer ran)
  • 前一个 state 和这个 state 之间的差异 (The diff between the previous state and this state)
  • 如果启用,函数堆栈跟踪会返回到首先调用 store.dispatch() 的代码行 (If enabled, the function stack trace leading back to the line of code that called store.dispatch() in the first place)

这是我们发送 “add todo” action 后 state 和 diff 选项卡的样子:

image.png

image.png

这些是非常强大的工具,可以帮助我们 dispatch 应用程序并准确了解内部发生的事情。

6. 总结

  • Redux 应用程序始终只有一个 store
    • 使用 Redux createStore API 创建 store
    • 每个 store 都有一个独立的根 reducer 方法
  • Stores 主要有三种方法
    • getState 返回当前 state
    • dispatch 向 reducer 发送一个 action 来更新 state
    • subscribe 接受一个监听器回调,该回调在每次 dispatch action 时运行
  • Store enhancers 让我们能够在创建 store 时进行自定义操作
    • Enhancers 包装了 store 并且可以覆盖它的方法
    • createStore 接受一个 enhancer 作为参数
    • 可以使用 compose API 将多个 enhancers 合并在一起
  • Middleware 是自定义 store 的主要方式
    • 使用 applyMiddleware enhancer 添加 middleware
    • Middleware 被写成三个相互嵌套的函数
    • 每次 dispatch action 时都会运行 middleware
    • Middleware 内部可能有副作用
  • Redux DevTools 可让你查看应用程序随时间发生的变化
    • DevTools 扩展可以安装在你的浏览器中
    • Store 需要添加 DevTools enhancer,使用 composeWithDevTools
    • DevTools 显示已 dispatch action 和 state 随时间的变化

四、React-Redux

1. Redux 和 UI 的基本集成

Redux 是一个独立的 JS 库。正如前文所述,即使没有设置用户界面,也可以创建和使用 Redux store。这也意味着 Redux 可以和任何 UI 框架一起使用(甚至不使用 任何 UI 框架),并且同时支持在客户端和服务器上使用。你可以使用 React、Vue、Angular、Ember、jQuery 或 vanilla JavaScript 编写 Redux 应用程序。

Redux 是专门为 React 设计的。React 允许你将 UI 描述为 state 函数,然后 Redux 控制并更新 state 以响应 action。

将 Redux 与任何 UI 层一起使用都需要这几个步骤:

  1. 创建 Redux store

  2. 订阅(subscribe)更新

  3. 在订阅回调内部:

    3.1. 获取当前 store 的 state

    3.2. 提取当前 UI 需要的数据

    3.3. 使用这些数据更新 UI

  4. 如有必要,使用初始 state 渲染 UI

  5. 通过 dispatch action 来响应 UI 的输入操作

回顾[最初的例子](##5. 基础使用实践)基本步骤:

// 1) 使用 `createStore` 函数创建 Redux store
const store = Redux.createStore(counterReducer)

// 2) 订阅更新(以便将来数据更改时能够重绘)
store.subscribe(render)

// 我们的“用户界面”是单个 HTML 元素中的一些文本
const valueEl = document.getElementById('value')

// 3) 当订阅回调函数运行时:
function render() {
  // 3.1) 获取当前 store 的 state
  const state = store.getState()
  // 3.2) 提取你想要的数据
  const newValue = state.value.toString()

  // 3.3) 使用新值更新 UI
  valueEl.innerHTML = newValue
}

// 4) 使用初始 state 渲染 UI
render()

// 5) 基于 UI 输入 dispatch action
document.getElementById('increment').addEventListener('click', function () {
  store.dispatch({ type: 'counter/incremented' })
})

无论使用什么 UI 层,Redux 都以相同的方式与每个 UI 一起工作。出于性能的考虑,实际实现通常会更复杂,但都遵循相同的步骤。

由于 Redux 是一个单独的库,因此有不同的绑定库可以帮助你将 Redux 与给定的 UI 框架一起使用。这些 UI 绑定库会处理订阅 store 的详细信息,并在 state 更改时有效地更新 UI,因此你不必自己编写这部分代码。

2. 在React中使用Redux

2.1 使用 useSelector 从 Store 中读取 State

useSelector是React-Redux其中的一个hook,它使得 React 组件可以从 Redux store 中读取数据

useSelector 接收一个 selector 函数。selector 函数接收 Redux store 的 state 作为其参数,然后从 state 中取值并返回

例如,在 todo 应用程序中,Redux state 将 todos 数组保存为 state.todos。我们可以编写一个 selector 函数来返回它:

const selectTodos = state => state.todos

也可以获取到当前被标记为 completed 的 todo 列表:

const selectTotalCompletedTodos = state => {
  const completedTodos = state.todos.filter(todo => todo.completed)
  return completedTodos.length
}

具体使用案例:

// reducer.js

import { combineReducers } from 'redux'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const rootReducer = combineReducers({
  todos: todosReducer,
  filters: filtersReducer
})

export default rootReducer
// store.js

import { legacy_createStore } from 'redux'
import rootReducer from './reducer'

const store = createStore(rootReducer)
export default store
// src/features/todos/TodoList.js

import React from 'react'
// ① 引入useSelector
import { useSelector } from 'react-redux'
const TodoList = () => {
  // ② 获取state.todos
  const todos = useSelector(selectTodos)
}

export default TodoList
// index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux' 
import './index.css'
import App from './App'
import store from './store'

// 全局派发store
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
)

在vanilla JavaScript 编写的Redux 应用程序中,需要调用 store.subscribe() 来监听每个组件中 store 的变化,但是这样会变得重复且难以控制。

而使用**useSelector 会自动订阅 Redux store!**这样,任何时候 dispatch action,它都会立即再次调用对应的 selector 函数。如果 selector 返回的值与上次运行时相比发生了变化,useSelector 将强制组件使用新值重新渲染。我们仅需要在组件中调用一次 useSelector() 即可。

注意点:

**useSelector 使用严格的 === 来比较结果,因此只要 selector 函数返回的结果是新地址引用,组件就会重新渲染!**这意味着如果在 selector 中创建并返回新地址引用,那么每次 dispatch action 后组件都会被重新渲染,即使数据值确实没有改变。

例如,将此 selector 传递给 useSelector 会导致组件 总是 被重新渲染,因为 array.map() 永远返回一个新的数组引用:

// 不好的示例:总是返回一个新的引用
const selectTodoDescriptions = state => {
  // 这会创建一个新的数组引用!
  return state.todos.map(todo => todo.text)
}

2.2 使用 useDispatch 来 Dispatch Action

在 React 之外我们可以调用 store.dispatch(action)给store派发事件,但是在组件文件中无法访问 store,因此我们需要一些方法来访问组件内部的 dispatch 函数。

React-Redux 的**useDispatch hook 函数**会返回 store 的 dispatch 方法。(事实上这个 hook 的内部实现真的是 return store.dispatch。)

因此,我们可以在任何需要 dispatch action 的组件中使用 const dispatch = useDispatch(),然后根据需要调用 dispatch(someAction)

我们将编写一个典型的 React 表单组件,它使用“受控组件(controlled inputs)”来让用户输入文本。然后,当用户按下 Enter 键时,就 dispatch action。

// src/features/header/Header.js

import React, { useState } from 'react'
// ① 引入useDispatch
import { useDispatch } from 'react-redux'

const Header = () => {
  const [text, setText] = useState('')
  //② 获取dispatch对象
  const dispatch = useDispatch()

  const handleChange = e => setText(e.target.value)

  const handleKeyDown = e => {
    const trimmedText = e.target.value.trim()
    // 如果用户按下 Enter 键:
    if (e.key === 'Enter' && trimmedText) {
      // ③ 使用这个文本来 dispatch "todo added" action
      dispatch({ type: 'todos/todoAdded', payload: trimmedText })
      // 清空文本输入框
      setText('')
    }
  }

  return (
    <input
      type="text"
      placeholder="What needs to be done?"
      autoFocus={true}
      value={text}
      onChange={handleChange}
      onKeyDown={handleKeyDown}
    />
  )
}

export default Header

2.3 使用 Provider 透传 Store

使用 <Provider> 组件包裹 <App> 组件,并将 Redux store 作为 prop 传递给 <Provider> 组件。之后,应用程序中的每个组件都可以在需要时能够访问到 Redux store。

我们把以上逻辑加到 index.js 文件中:

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'

import App from './App'
import store from './store'

ReactDOM.render(
  // 使用 `<Provider>` 组件包裹 `<App>` 组件
  // 并把 Redux store 作为 prop 传入
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
)

3. 全局State与组件State

整个应用程序所需的全局 state 应该放在 Redux store 中。只在一个组件内使用的 state 应该放在组件 state 中。

重点:在 React + Redux 应用程序中,全局 state 应该放在 Redux store 中,本地 state 应该留在 React 组件中。

如果你不确定哪些数据应该放入 Redux,这里有一些常用的经验法则可以参考:

  • 应用程序的其他部分是否关心这些数据?
  • 是否需要基于这些原始数据创建派生数据?
  • 是否使用相同的数据来驱动多个组件?
  • 是否有将此 state 恢复到特定时间点(即时间旅行调试)的需求?
  • 是否需要缓存数据(比如它已经存在,则直接使用 state 中的值而不重新请求)?
  • 是否希望在热重载 UI 组件时(可能会丢失内部 state) 仍能保持数据一致性?

这也是一个如何在 Redux 中设计表单 state 很好的例子。大多数表单 state 可能不应该保存在 Redux 中。 而是应该在编辑表单时将数据保存在表单组件里,然后在用户完成时 dispatch action 以更新 store。

4. 总结

  • Redux stores 可以和任何 UI 层一起使用
    • UI 代码始终订阅 store 以获取最新的 state,并自行重绘
  • React-Redux 是 React 的官方 Redux UI 绑定库
    • React-Redux 作为单独的 react-redux 包安装
  • useSelector hook 使得 React 组件能够从 store 中读取数据
    • selector 函数将整个 store state 作为参数,并根据该 state 返回一个值
    • useSelector 调用它的 selector 函数并返回 selector 返回的结果
    • useSelector 订阅 store,并在每次 dispatch action 时重新运行 selector
    • 每当 selector 结果发生变化时,useSelector 将强制组件使用新数据重新渲染
  • useDispatch hook 使得 React 组件能够向 store dispatch action
    • useDispatch 返回实际的 store.dispatch 函数
    • 你可以根据需要在组件内部调用 dispatch(action)
  • <Provider> 组件使其他 React 组件可以和 store 进行交互
    • 使用 <Provider store={store}> 组件包裹 <App>

五、Redux-Thunk(Redux异步操作)

1. Redux异步数据流

Redux store 本身无法处理异步逻辑。它只会同步地 dispatch action,并通过调用根 reducer 函数来更新 state,然后通知视图更新。任何异步都必须在 store 之外发生。

之前提过 Redux reducer 绝对不能包含“副作用”。“副作用”是指除函数返回值之外的任何变更,包括 state 的更改或者其他行为。一些常见的副作用是:

  • 在控制台打印日志
  • 保存文件
  • 设置异步定时器
  • 发送 AJAX HTTP 请求
  • 修改存在于函数之外的某些 state,或改变函数的参数
  • 生成随机数或唯一随机 ID(例如 Math.random()Date.now()

Redux middleware 就是用来放这些副作用逻辑代码的地方

当 Redux middleware 执行 dispatch action 时,它可以做 任何事情:记录某些内容、修改 action、延迟 action,进行异步调用等。此外,由于 middleware 围绕真正的 store.dispatch 函数形成了一个管道,这也意味着我们实际上可以将一些 不是 普通 action 对象的东西传递给 dispatch,只要 middleware 截获该值并且不让它到达 reducer。

Middleware 也可以访问 dispatchgetState。这意味着你可以在 middleware 中编写一些异步逻辑,并且仍然能够通过 dispatch action 与 Redux store 进行交互。

middleware 和异步逻辑如何影响 Redux 应用程序的整体数据流的呢?

就像普通 action 一样,首先需要在应用程序中处理用户事件,比如点击按钮。然后,调用 dispatch(),并传入 一些内容,无论是普通的 action 对象、函数,还是 middleware 可以找到的其他值。

一旦 dispatch 的值到达 middleware,它就可以进行异步调用,然后在异步调用完成时 dispatch 一个正真的 action 对象。

前面,我们看了Redux 同步数据流程图。当我们向 Redux 应用程序添加异步逻辑时,额外添加了 middleware 步骤,可以在其中运行 AJAX 请求等逻辑,然后 dispatch action。这使得异步数据流看起来像这样:

ReduxAsyncDataFlowDiagram-d97ff38a0f4da0f327163170ccc13e80.gif

2. Redux Thunk Middleware

在没有redux-thunk middleware之前,我们每一次在redux中写异步任务都需要自己手写一个处理异步任务的middleware(官网解释)。而实际上,Redux 已经有了异步函数 middleware 的正式版本,称为 Redux “Thunk” middleware。thunk middleware 允许我们编写以 dispatchgetState 作为参数的函数。thunk 函数可以包含我们想要的任何异步逻辑,并且该逻辑可以根据需要 dispatch action 以及读取 store state。

2.1 配置Store

Redux thunk middleware 在 NPM 上作为一个名为 redux-thunk 的包提供。需要安装该软件包后才能在应用程序中使用:

npm install redux-thunk

安装后,我们可以更新 todo 应用程序中的 Redux store 来使用该 middleware:

// src/store.js

import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'

const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))

// store 现在就可以在 `dispatch` 中接收 thunk 函数了
const store = createStore(rootReducer, composedEnhancer)
export default store

2.2 从服务器获取 Todos

目前我们的 todo 条目只能存在于客户端的浏览器中。我们需要一种在应用启动时从服务器加载 todos 列表的方法。

首先编写一个 thunk 函数,该函数向 /fakeApi/todos 端点进行 AJAX 请求获取到一个 todo 对象数组,然后 dispatch 包含该数组作为 payload 的 action。因为这部分内容和 todos 功能相关,所以我们在 todosSlice.js 文件中编写 thunk 函数

// src/features/todos/todosSlice.js

import { client } from '../../api/client'

const initialState = []

export default function todosReducer(state = initialState, action) {
  // 省略 reducer 逻辑
}

// Thunk 函数
export async function fetchTodos(dispatch, getState) {
  const response = await client.get('/fakeApi/todos')
  dispatch({ type: 'todos/todosLoaded', payload: response.todos })
}

我们只需要在应用程序第一次加载时调用这个 API。以下是 可以 放这个 thunk 函数的地方:

  • <App> 组件的 useEffect hook 函数中
  • <TodoList> 组件的 useEffect hook 函数中
  • 直接放在 index.js 导入 store 之后的地方

现在,尝试将其直接放在 index.js 中:

// src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import './index.css'
import App from './App'

import './api/server'

import store from './store'
import { fetchTodos } from './features/todos/todosSlice'

store.dispatch(fetchTodos)

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

重新加载页面后,虽然 UI 没有变化,但如果我们打开 Redux DevTools,就可以看到一个 'todos/todosLoaded' action 被 dispatch 了,并且它包含一些由虚拟服务器 API 生成的 todo 对象:

image.png

请注意,即使我们已经 dispatch 了一个 action,state 仍没有任何改变。我们需要在 todos reducer 中处理这个 action 来更新 state。

让我们在 reducer 中添加一个 case,来将这些数据载入 store。因为我们需要从服务器获取数据,并完全替换现有的 todos,所以可以返回 action.payload 数组,使其成为新的 todos state 值:

// src/features/todos/todosSlice.js

import { client } from '../../api/client'

const initialState = []

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    // 省略其他 reducer cases
    case 'todos/todosLoaded': {
      // 通过返回的新值完全替换现有 state
      return action.payload
    }
    default:
      return state
  }
}

export async function fetchTodos(dispatch, getState) {
  const response = await client.get('/fakeApi/todos')
  dispatch({ type: 'todos/todosLoaded', payload: response.todos })
}

由于 dispatch action 会立即更新 store,所以我们也可以在 dispatch 后调用 thunk 中的 getState 来读取更新后的 state 值。例如,我们可以在 dispatch 'todos/todosLoaded' 操作前后将 todos 总数打印到控制台:

export async function fetchTodos(dispatch, getState) {
  const response = await client.get('/fakeApi/todos')

  const stateBefore = getState()
  console.log('Todos before dispatch: ', stateBefore.todos.length)

  dispatch({ type: 'todos/todosLoaded', payload: response.todos })

  const stateAfter = getState()
  console.log('Todos after dispatch: ', stateAfter.todos.length)
}

2.3 保存 Todo 条目

每次创建新的 todo 条目后,都需要更新服务器的数据。我们应该使用初始数据对服务器进行 API 调用,等待服务器返回新保存的 todo 条目副本,然后 使用那些 todo 条目来 dispatch action,而不是立即 dispatch 'todos/todoAdded' action。

但是,尝试将此逻辑编写为 thunk 函数,将会遇到一个问题:由于我们将 thunk 编写成 todosSlice.js 文件中的独立函数,所以进行 API 调用的代码不知道新的 todo 文本应该是什么:

// src/features/todos/todosSlice.js

async function saveNewTodo(dispatch, getState) {
  // ❌ 我们需要有新的 todo 文本,但它来自哪里?
  const initialTodo = { text }
  const response = await client.post('/fakeApi/todos', { todo: initialTodo })
  dispatch({ type: 'todos/todoAdded', payload: response.todo })
}

我们需要实现一个接受 text 作为参数的函数,接着创建实际的 thunk 函数,以便它可以使用 text 值进行 API 调用。然后,外部函数应该返回 thunk 函数,以便可以传递给组件中的 dispatch

// src/features/header/Header.js

// 编写一个接收 `text` 参数的同步外部函数:
export function saveNewTodo(text) {
  // 然后创建并返回异步 thunk 函数:
  return async function saveNewTodoThunk(dispatch, getState) {
    // ✅ 现在可以使用文本值并将其发送到服务器了
    const initialTodo = { text }
    const response = await client.post('/fakeApi/todos', { todo: initialTodo })
    dispatch({ type: 'todos/todoAdded', payload: response.todo })
  }
}

现在我们可以在 <Header> 组件中使用它:

// src/features/header/Header.js

import React, { useState } from 'react'
import { useDispatch } from 'react-redux'

import { saveNewTodo } from '../todos/todosSlice'

const Header = () => {
  const [text, setText] = useState('')
  const dispatch = useDispatch()

  const handleChange = e => setText(e.target.value)

  const handleKeyDown = e => {
    // 如果用户按下 Enter 键:
    const trimmedText = text.trim()
    if (e.which === 13 && trimmedText) {
      // 使用用户编写的文本创建 thunk 函数
      const saveNewTodoThunk = saveNewTodo(trimmedText)
      // 然后 dispatch thunk 函数本身
      dispatch(saveNewTodoThunk)
      setText('')
    }
  }

  // omit rendering output
}

因为我们将立即传递 thunk 函数给组件中的 dispatch,所以可以跳过创建临时变量的步骤。相反,我们可以调用 saveNewTodo(text),并将生成的 thunk 函数直接传递给 dispatch

// src/features/header/Header.js

const handleKeyDown = e => {
  // 如果用户按下 Enter 键:
  const trimmedText = text.trim()
  if (e.which === 13 && trimmedText) {
    // 创建 thunk 函数并立即 dispatch
    dispatch(saveNewTodo(trimmedText))
    setText('')
  }
}

现在组件实际上并不知道它甚至在 dispatch 一个 thunk 函数 - saveNewTodo 函数正在封装实际发生的事情。<Header> 组件只知道它需要在用户按下回车键时 dispatch 一些值

编写预备传递给dispatch内容的函数,这一模式称为action creator 模式,我们将在下一节详细讨论。

现在可以看到更新后的 'todos/todoAdded' action 被 dispatch 了:

image.png

这里最后需要做的事情是更新 todos reducer。当我们向 /fakeApi/todos 发出 POST 请求时,服务器将返回一个全新的 todo 对象(包括一个新的 ID 值)。这意味着 reducer 不必计算新的 ID,或填充其他字段 - 它只需要创建一个包含新 todo 条目的新 state 数组:

// src/features/todos/todosSlice.js

const initialState = []

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    case 'todos/todoAdded': {
      // 返回一个新的 todos state 数组,最后带有新的 todo 条目
      return [...state, action.payload]
    }
    // 省略其他 case
    default:
      return state
  }
}

现在添加一个新的 todo 将会正常工作:

Devtools - async todoAdded state diff转存失败,建议直接上传图片文件

Thunk 函数可用于处理异步 同步逻辑。Thunks 提供了一种方法来编写任何需要访问 dispatchgetState 的可重用逻辑。

3. 简单使用

// reducerSlice.js

const initialState = []

export default todoReducer(state = initialState)
default function todosReducer(state = initialState, action) {
  switch (action.type) {
    // ...其他action type
    case 'play': {
      console.log('play games!!!')
      return state
    }
    default:
      return state
  }
}
// reducer

//使用combineReducers统一暴露管理多个reducer slice
import { combineReducers } from 'redux'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const rootReducer = combineReducers({
  // 定义一个名为`todos`的顶级状态字段,由`todosReducer`处理
  todos: todosReducer,
  filters: filtersReducer
})

export default rootReducer
// store

import { legacy_createStore as createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'

// 引用异步中间件
const composedEnhancer = composeWithDevTools(
  applyMiddleware(thunkMiddleware)
)
const store = createStore(rootReducer, composedEnhancer)

// 异步函数
const play = async (dispatch, getState) => {
  //异步函数
  await new Promise(resolve => {
    setTimeout(()=>{
      console.log('异步2秒后响应')
      resolve()
    },2000)
  }, 5000)
  dispatch({type: 'play'})
}
store.dispatch(play) 

export default storestore

4. 总结

  • Redux middleware 旨在支持编写具有副作用的逻辑
    • “副作用”是指更改函数外部 state 或行为的代码,例如 AJAX 调用、修改函数参数或生成随机值
  • middleware 为标准 Redux 数据流增加了一个额外的步骤
    • middleware 可以拦截传递给 dispatch 的其他值
    • middleware 可以访问 dispatchgetState,因此它们可以作为异步逻辑的一部分 dispatch 更多 action
  • Redux “Thunk” middleware 使得可以传递函数给 dispatch
    • “Thunk” 函数让我们可以提前编写异步逻辑,而不需要知道当前使用的 Redux store
    • Redux thunk 函数接收 dispatchgetState 作为参数,并且可以 dispatch 诸如“此数据是从 API 响应中接收到的”之类的 action

六、现代Redux:Redux Toolkit

依赖包:

npm i @reduxjs/toolkit react-reudx

1. 设置Store

1.1 以往:

// src/rootReducer.js

import { combineReducers } from 'redux'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const rootReducer = combineReducers({
  // 定义一个名为 `todos` 的顶级 state 字段,值为 `todosReducer`
  todos: todosReducer,
  filters: filtersReducer
})

export default rootReducer
// src/store.js

import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'

const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))

const store = createStore(rootReducer, composedEnhancer)
export default store

设置过程需要几个步骤。我们必须:

  • 将 slice reducers 组合在一起形成根 reducer
  • 将根 reducer 导入到 store 文件中
  • 导入 thunk middlewareapplyMiddlewarecomposeWithDevTools API
  • 使用 middleware 和 devtools 创建 store enhancer
  • 使用根 reducer 创建 store

1.2 如今:

1.2.1. 使用 configureStore

Redux Toolkit 的 configureStore API,可简化 store 的设置过程configureStore 包裹了 Redux 核心 createStore API,并自动为我们处理大部分 store 设置逻辑。事实上,我们可以有效地将其缩减为一步:

// src/store.js

import { configureStore } from '@reduxjs/toolkit' 

import counterReducer from "./features/couterSlice";
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const store = configureStore({
  reducer: {
    // 定义一个名为 `todos` 的顶级 state 字段,值为 `todosReducer`
    counter: counterReducer,
    todos: todosReducer,
    filters: filtersReducer
  }
})

export default store

configureStore 为我们完成了所有工作:

  • counterReducertodosReducerfiltersReducer 组合到根 reducer 函数中,它将处理看起来像 {todos, filters} 的根 state
  • 使用根 reducer 创建了 Redux store
  • 自动添加了 “thunk” middleware
  • 自动添加更多 middleware 来检查常见错误,例如意外改变(mutate)state
  • 自动设置 Redux DevTools 扩展连接

2.编写Reducer Slices

2.1 以往:

// src/feature/counterSlice

const initialState = {
  value: 0
}

const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'counter/increment':
      return {value : state.value + 1}
    case 'counter/decrement':
      return {value : state.value - 1}
    default:
      return state;
  }
}

export default counterReducer

需要手动完成的工作:

  • 需要自己手动去编写大量的 switch/case 语句
  • 由于state的不可变性我们返回的state需要通过复制副本后再操作

2.2 如今:

2.2.1. 使用createSlice

随着我们不断向应用程序添加新功能,slice 文件变得更大更复杂了。特别是 todosReducer 变得更难阅读,因为扩展所有的嵌套对象都是为了不可变更新,而且我们已经编写了多个 action creator 函数。

Redux Toolkit 的 createSlice API,将帮助我们简化 Redux reducer 逻辑和 actionscreateSlice 为我们做了几件重要的事情:

  • 可以将 case reducer 编写为对象内部的函数,而不必编写 switch/case 语句
  • reducer 将能够编写更短的不可变更新逻辑
  • 所有 action creators 将根据我们提供的 reducer 函数自动生成

createSlice 接收一个包含三个主要选项字段的对象:

  • name:一个字符串,将用作生成的 action types 的前缀
  • initialState:reducer 的初始 state
  • reducers:一个对象,其中键是字符串,值是处理特定 actions 的 “case reducer” 函数
// src/feature/couterSlice.js

import { createSlice } from "@reduxjs/toolkit"
const initialState = {
  value: 0
}

const counterReducer = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    // 这里对应的就是不同action
    increment(state, action) {
      // 默认返回的就是state
      // 不需要复制副本,可以直接操作state的值
      state.value++
    },
    decrement(state, action) {
      state.value--
    }
  }
})

//分别导出操作函数(actions)
export const {
  increment,
  decrement
} = counterReducer.actions

//默认导出整一个 Reducer
export default counterReducer.reducer

示例中可以看到几件事:

  • 我们在 reducers 对象中编写 case reducer 函数,并赋予它们高可读性的名称
  • createSlice 会自动生成对应于每个 case reducer 函数的 action creators
  • createSlice 在默认情况下自动返回现有 state
  • createSlice 允许我们安全地“改变”(mutate)state!
  • 但是,如果愿意,我们也可以像以前一样拷贝不可变副本

生成的 action creators 将作为 slice.actions.todoAdded 提供,我们通常像之前编写的 action creators 一样单独解构和导出它们。完整的 reducer 函数可以作为 slice.reducer 使用,和之前一样,我们通常会 export default slice.reducer

2.2.2. 调用reducer中的方法

// src/App.js

import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from './features/couterSlice'

function App() {
  const coute = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()
  return (
    <div className="App">
        {/* 方式一 */}
        <button onClick={() => dispatch({type: 'counter/increment'})}>+</button>
        {/* 方式二 */}
        <button onClick={() => dispatch(increment())}>+</button>
        <span style={{margin: '0 10px'}}>{coute}</span>
        <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  )
}

export default App

2.2.3. dispatch传递参数

与以往的action一样,action 一般也会接收一个payload属性用于接收操作参数。

1、传递单个参数:

// src/App.js

import { useSelector, useDispatch } from 'react-redux'
import { customIncrement } from './features/couterSlice'

function App() {
  const coute = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()
  return (
    <div className="App">
     	{/* 传值单个值 */}
        <button onClick={() => dispatch(customIncrement(2))}>自增2</button>
        <span style={{margin: '0 10px'}}>{coute}</span>
    </div>
  )
}

export default App


// src/feature/couterSlice.js
import { createSlice } from "@reduxjs/toolkit"
const initialState = {
  value: 0
}

const counterReducer = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    customIncrement(state, action) {
      console.log('自定义自增数值:', action.payload);
      state.value += action.payload
    },
  }
})

//分别导出操作函数(actions)
export const { customIncrement } = counterReducer.actions

//默认导出整一个 Reducer
export default counterReducer.reducer

2、传递多个参数

// src/App.js

import { useSelector, useDispatch } from 'react-redux'
import { customIncrement, customDecrement } from './features/couterSlice'

function App() {
  const coute = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()
  return (
    <div className="App">
        {/* 传递单个参数 */}
        <button onClick={() => dispatch(customIncrement(2))}>自增2</button>
        <span style={{margin: '0 10px'}}>{coute}</span>
        {/* 传递多个参数 */}
        <button onClick={() => dispatch(customDecrement(2, 'customDecremtn'))}>自减2(多个参数)</button>
    </div>
  )
}

export default App

// src/feature/couterSlice.js

import { createSlice } from "@reduxjs/toolkit"
const initialState = {
  value: 0
}

const counterReducer = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    //接收单个参数
    customIncrement(state, action) {
      console.log('自定义自增数值:', action.payload);
      state.value += action.payload
    },
    //接收多个参数
    customDecrement: {
      reducer(state, action) {
        console.log('自定义自减名:', action.payload.name);
        state.value -= action.payload.value
      },
      //dispatch customDecrement时,首先会先执行prepare函数把
      //传递过来的参数合成一个最终的payload,再触发reducer,并把
      //合并好的payload传递给reducer
      prepare(value, name) {
        return {
          payload : { value, name }
        } 
      }
    }
  }
})

//分别导出操作函数(actions)
export const {
  customIncrement,
  customDecrement
} = counterReducer.actions

//默认导出整一个 Reducer
export default counterReducer.reducer