从零开始入门 Redux

223 阅读12分钟

初始化项目

使用 Create React App 进行项目初始化

npx create-react-app redux-demo

Redux工作流

要熟练的使用 Redux, 首先得熟悉 Redux 的工作流程。下面的图例完整的展示了 Redux 的全部工作流程,初次接触的时候只需要心中有个大概就行,后续工作使用 Redux 中,会发现就是反复在重复这个流程

12201495-649ed8e1c66af6d3.webp 分别介绍下各个流程的含义:

Action

一个用于描述要干什么动作的普通对象。其中 type 属性是必需的

{
  type: 'ADD_TODO', // type字段必须
  text: '早晨八点开始晨会'
}

Reducer

根据Action动作的不同,对仓库的数据做处理的一个纯函数
纯函数的定义

  1. 如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。
  2. 该函数不会产生任何可观察的副作用,例如网络请求,输入和输出设备或数据突变(mutation)。
function counterReducer(state = initialState, action) {
  switch (action.type) {
    case "counter/incremented":
      return { ...state, value: state.value + 1 };
    case "counter/decremented":
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}

Reducer 内不修改 state 数据,只返回一个全新的数据状态

Store

仓库,负责存放状态(state),将 actions 与 reducers 联系起来的东西。
它的功能点有:

  • 维持应用的 state;
  • 提供 getState()方法获取 state;
  • 提供 dispatch(action)方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。

从一个计数器开始

跟着官网按理简单的实现下 UI

import React, { Component } from 'react';

export default class App extends Component {
  render() {
    return (
      <div className="App">
        <p>
          数量: <span id="value">0</span>
          <button id="increment">+</button>
          <button id="decrement">-</button>
        </p>
      </div>
    );
  };
};

可以看到界面中有一个用来展示数量的容器和两个按钮,接下来我们就使用 Redux 来实现数量的加减与显示

Redux 初始化

首先我们需要创建一个文件夹 store, 并创建文件 index.js 用来初始化 Redux

第一步:安装 Redux

npm install redux --save

第二步:定义状态(state)

定义应用程序的初始状态值
即声明仓库中应该有哪些数据,初始值是什么

const initialState = {
  value: 0
};

第三步:定义动作(action)

定义应用程序的状态修改动作
我们应该事先定义好我们应该可以运行状态可以进行哪些动作修改,这里计数器可以进行的分别是加和减

const counterIncrementedAction = () => ({ type: 'counter/incremented' })
const counterDecrementedAction = () => ({ type: 'counter/decremented' })

在这里我们分别声明了两个动作:加和减

第四步:创建 reducer

创建一个"reducer"函数来确定新的状态

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case "counter/incremented":
      return { ...state, value: state.value + 1 };
    case "counter/decremented":
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}

这里是定义动作的具体实现,也就是根据动作的不同返回不同的状态。(注意:这里还不能修改状态,只是返回一个全新的状态

第五步:创建仓库(store)

Redux 提供了一个方法 createStore,可以使用这个方法进行仓库创建,他需要提供一个 Reducer

import { createStore } from 'redux'
// 其它逻辑...
const store = createStore(counterReducer);
export default store

注意:一个项目只能创建一个仓库(store)

至此我们完成了 Redux 的初始化,接下来我们开始使用它吧。

使用 Redux

回到刚开始的 UI 界面,我们需要把仓库中的状态展示在数量容器中,并实现加减运算

第一步:获取数据

首先我们需要获取到仓库中的状态,我们可以通过 getState 获取到仓库中的状态

import React, { Component } from 'react';
import store from "./store";

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = store.getState()
  }
  render() {
    return (
      <div className="App">
        <p>
          数量: <span id="value">{this.state.value}</span>
          <button id="increment">+</button>
          <button id="decrement">-</button>
        </p>
      </div>
    );
  };
};
  1. 首先将仓库中的值全部取出,并在组件的构造函数中为 this.state 赋初值
  2. 然后在数量容器中渲染该 value

第二步:触发动作

触发动作需要使用 dispatch 方法来更新状态

// 将之前定义的动作引入并执行传入给 disapatch 方法
incremented() {
  store.dispatch(counterIncrementedAction())
}

给按钮绑定点击事件

<button id="increment" onClick={this.incremented}>+</button>

这时点击后会进行更新状态

第三步:Redux DevTools

在浏览器调试时,可以使用谷歌插件 Redux DevTools 进行查看状态的变更

下载需要科学上网后在谷歌应用商店安装 Redux DevTools

下载完成后改造 store,在使用 createStore 方法时添加一个参数

const store = createStore(counterReducer, window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f);

现在可以使用插件调试发现我们点击 + 号,仓库状态可以成功 +1

第四步:订阅变更

在点击 + 号后仓库中状态成功更改了,但是视图中数据并未更改,此时还需要进行状态变更订阅

componentDidMount() {
  store.subscribe(() => {
    this.setState(store.getState())
  })
}

componentDidMount 生命周期函数中进行监听订阅状态变更后更新当前组件的状态

至此一个的 Redux 使用案例就全部完成了

完整代码:

// APP组件, app.js
import React, { Component } from 'react';
import store, { counterIncrementedAction, counterDecrementedAction } from "./store";

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = store.getState()
  }
  componentDidMount() {
    store.subscribe(() => {
      this.setState(store.getState())
    })
  }
  incremented() {
    store.dispatch(counterIncrementedAction())
  }
  decremented() {
    store.dispatch(counterDecrementedAction())
  }
  render() {
    return (
      <div className="App">
        <p>
          数量: <span id="value">{this.state.value}</span>
          <button id="increment" onClick={this.incremented}>+</button>
          <button id="decrement" onClick={this.decremented}>-</button>
        </p>
      </div>
    );
  };
};
// store/index.js
import { createStore } from 'redux'
const initialState = {
  value: 0
}

export const counterIncrementedAction = () => ({ type: 'counter/incremented' })
export const counterDecrementedAction = () => ({ type: 'counter/decremented' })

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case "counter/incremented":
      return { ...state, value: state.value + 1 };
    case "counter/decremented":
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}

const store = createStore(counterReducer, window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f);

export default store

Redux 高阶应用

就其本身而言,Redux store 对异步逻辑一无所知。它只知道如何同步 dispatch action,通过调用 root reducer 函数更新状态,并通知 UI 某些事情发生了变化。任何异步都必须发生在 store 之外。

但是,如果您希望通过调度或检查当前 store 状态来使异步逻辑与 store 交互,该怎么办? 这就是 Redux 中间件 的用武之地。它们扩展了 store,并允许您:

  • dispatch action 时执行额外的逻辑(例如打印 action 的日志和状态)
  • 暂停、修改、延迟、替换或停止 dispatch 的 action
  • 编写可以访问 dispatch 和 getState 的额外代码
  • 教 dispatch 如何接受除普通 action 对象之外的其他值,例如函数和 promise,通过拦截它们并 dispatch 实际 action 对象来代替

Redux 使用时在派发(dispatch)一个动作(action)给仓库(store)后,就完全交由仓库来变更状态(state),如果我们想要在仓库变更状态之前(reducer 之前)执行一些动作的话,可以使用中间件(middleware

早些时候,我们看到了Redux 的同步数据流是什么样子。当我们引入异步逻辑时,我们添加了一个额外的步骤,中间件可以运行像 AJAX 请求这样的逻辑,然后 dispatch action。这使得异步数据流看起来像这样:

ReduxAsyncDataFlowDiagram-d97ff38a0f4da0f327163170ccc13e80.gif

Redux Thunk

安装

npm install redux-thunk --save

插件配置

如果需要使用中间件,需要使用 redux 中的 applyMiddleware

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

// 一些其它逻辑...
const store = createStore(counterReducer, applyMiddleware(thunk));

如果需要配合使用 Redux Dev Tools 插件使用就需要额外配置一个增强函数,Redux 提供了一个 compose 方法来轻松帮我们实现一个增强函数

import { createStore , applyMiddleware ,compose } from 'redux'
import thunk from 'redux-thunk'

// 一些其它逻辑...

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose
const enhancer = composeEnhancers(applyMiddleware(thunk))

const store = createStore(counterReducer, enhancer);

Thunk 函数

将 thunk 中间件添加到 Redux store 后,它允许您将 thunk 函数 直接传递给 store.dispatch。调用 thunk 函数时总是将 (dispatch, getState) 作为它的参数,你可以根据需要在 thunk 中使用它们。

Thunks 通常还可以使用 action creator 再次 dispatch 普通的 action,比如 dispatch(counterIncrementedAction())

export const counterIncrementedThunk = (dispatch, getState) => {
  const stateBefore = getState()
  console.log(`Counter before: ${stateBefore.value}`)
  dispatch(counterIncrementedAction())
  const stateAfter = getState()
  console.log(`Counter after: ${stateAfter.value}`)
}

改造上述例子后:

<button onClick={this.incrementedThunk}>+</button>
incrementedThunk() {
  store.dispatch(counterIncrementedThunk)
}

点击后同样可以实现数据的更新

为了与 dispatch 普通 action 对象保持一致,我们通常将它们写为 thunk action creators,它返回 thunk 函数。这些 action creator 可以接受可以在 thunk 中使用的参数。

export const counterIncrementedThunk = () => {
  return (dispatch, getState) => {
    const stateBefore = getState()
    console.log(`Counter before: ${stateBefore.value}`)
    dispatch(counterIncrementedAction())
    const stateAfter = getState()
    console.log(`Counter after: ${stateAfter.value}`)
  }
}

使用时先执行函数

incrementedThunk() {
  store.dispatch(counterIncrementedThunk())
}

“thunk” 这个词是一个编程术语,意思是 "一段做延迟工作的代码"

上述函数方法还是同步方法,下面开始实现异步编程

编写异步 Thunks

  1. 首先我们增加一个改变状态的动作
export const counterChangeAction = (value) => ({ type: 'counter/change', value  })
  1. 动作逻辑实现
function counterReducer(state = initialState, action) {
  switch (action.type) {
    case "counter/change":
      return { ...state, value: action.value };
      // ... 其它逻辑
  }
}
  1. 编写 Thunk 异步函数
export const counterChangeThunk = () => {
  return (dispatch) => {
    fetch('/api/get').then(async res => {
      if (res.ok) {
        const { data } = await res.json()
        dispatch(counterChangeAction(data))
      }
    })
  }
}
  1. 使用 Thunk 异步函数
<button onClick={this.getValue}>获取远程数据</button>
getValue() {
  store.dispatch(counterChangeThunk())
}

点击按钮后就可以用服务器返回的数据来更新状态

combineReducers

随着应用变得越来越复杂,可以考虑将 reducer 函数 拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分。

todos.js

// reducers/modules/todos.js
export default function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}

counter.js

// reducers/modules/counter.js
export default function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

reducers/index.js

import { combineReducers } from 'redux'
import todos from './modules/todos'
import counter from './modules/counter'

export default combineReducers({
  todos,
  counter
})

App.js

import { createStore } from 'redux'
import reducer from './reducers/index'

let store = createStore(reducer)
console.log(store.getState())
// {
//   counter: 0,
//   todos: []
// }

store.dispatch({
  type: 'ADD_TODO',
  text: 'Use Redux'
})
console.log(store.getState())
// {
//   counter: 0,
//   todos: [ 'Use Redux' ]
// }

type 集中管理

现在这种方式在使用过程中如果 type 不小心在否个地方写错了是很难发现,而且容易照成命名冲突,所以比较好的办法是集中一个文件管理项目中 types,其它地方进行引入使用。

// reducers/actionTypes.js
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

export const ADD_TODO = 'ADD_TODO'

还可以根据 type 模块不同进行模块划分

// reducers/actionTypes.js

export const TODO_ACTION_TYPE = {
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT'
}

export const COUNTER_ACTION_TYPE = {
  ADD_TODO: 'ADD_TODO'
}

React-redux

Redux本身是一个独立的库,可以用于任何UI层或框架,包括React、Angular、Vue 和 vanilla JS。虽然Redux和React通常一起使用,但它们是相互独立的。

React-Redux是React的官方Redux UI绑定库。如果你同时使用Redux和React,你也应该使用React-Redux来绑定这两个库。

安装

npm install react-redux --save

Provider

Provider 节点包裹 <APP> 根节点,并将 store 作为 porps 值传递给 Provider ,之后在应用程序的每个组件都可以访问 Redux 存储了

import { Provider } from 'react-redux'
import store from './store'
const Root = ( <Provider store={store}> <App /> </Provider> )

connect 连接器

  1. 映射你需要使用的数据
const mapStateToProps = state => ({ value: state.counter.value })
  1. 使用连接器改造要导出的组件
export default connect(mapStateToProps,null)(App)
  1. 使用映射的数据
// 映射的数据会合并到组件 props 中
<span>{this.props.value}</span>
  1. 去除监听器
// componentDidMount() {
//   store.subscribe(() => {
//     this.setState({
//       value: store.getState().counter.value
//     })
//   })
// }

去除监听器会发现之前代码的数据视图未更改,但是通过映射展示的数据视图是可以正常更新的

派发动作

  1. 定义 dispatch 映射
const dispatchToProps = dispatch => ({
 incrementedThunk: () => {
   dispatch(counterIncrementedThunk())
 }
})
  1. 加入连接器
export default connect(mapStateToProps, dispatchToProps)(App)
  1. 使用映射的方法
<button onClick={this.props.incrementedThunk}>映射方法派发</button>

优化代码

优化完成后会发现该组件未无状态组件,那么可以改造成函数组件,优化性能

import React from 'react';
import { connect } from 'react-redux'
import {
  counterChangeAction,
  counterIncrementedAction,
  counterDecrementedAction,
  counterIncrementedThunk, counterChangeThunk
} from "./store/reducers/modules/counter";

const mapStateToProps = state => ({ value: state.counter.value })
const dispatchToProps = dispatch => ({
  incremented: () => {
    dispatch(counterIncrementedAction())
  },
  incrementedThunk: () => {
    dispatch(counterIncrementedThunk())
  },
  decremented: () => {
    dispatch(counterDecrementedAction())
  },
  valueChange: (e) => {
    dispatch(counterChangeAction(e.target.value))
  },
  getValue() {
    dispatch(counterChangeThunk())
  },
})

const App = (props) => {
  const { value, incremented, incrementedThunk, decremented, valueChange, getValue } = props
  return (
    <div className="App">
      <p>
        数量: <span id="value">{value}</span>
        <button id="increment" onClick={incremented}>+</button>
        <button id="decrement" onClick={decremented}>-</button>
      </p>
      <div>
        <span>Action:</span>
        <button onClick={incremented}>+</button>
        <button onClick={decremented}>-</button>
      </div>
      <div>
        <span>Thunk:</span>
        <button onClick={incrementedThunk}>+</button>
        <button onClick={decremented}>-</button>
      </div>
      <div>
        <button onClick={getValue}>获取远程数据</button>
      </div>
      <div>
        <input type="text" value={value} onChange={valueChange}/>
      </div>
      <p>
        <span>{value}</span>
      </p>
      <div>
        <button onClick={incrementedThunk}>映射方法派发</button>
      </div>
    </div>
  )
}

export default connect(mapStateToProps, dispatchToProps)(App)

优化后会发现逻辑结构简单明了

Redux Toolkit

Redux Toolkit包旨在成为编写Redux逻辑的标准方式。它最初的创建是为了帮助解决关于 Redux 的三个常见问题:

  • “配置 Redux 存储太复杂了”
  • “我必须添加很多包才能让 Redux 做任何有用的事情”
  • “Redux 需要太多样板代码”

使用 Create React

使用 React 和 Redux 启动新应用程序的推荐方法是使用官方 Redux+JS 模板或Redux+TS 模板来创建 React App,它利用了Redux Toolkit和 React Redux 与 React 组件的集成。

# Redux + Plain JS template
npx create-react-app my-app --template redux

# Redux + TypeScript template
npx create-react-app my-app --template redux-typescript

安装

# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit

使用

Redux Toolkit 包括以下 API:

  • configureStore(): 包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的切片缩减器,添加你提供的任何 Redux 中间件,redux-thunk默认包含,并允许使用 Redux DevTools Extension。
  • createReducer():这使您可以为 case reducer 函数提供操作类型的查找表,而不是编写 switch 语句。此外,它自动使用immer库让您使用普通的可变代码编写更简单的不可变更新,例如state.todos[3].completed = true.
  • createAction():为给定的动作类型字符串生成动作创建函数。该函数本身已toString()定义,因此可以使用它来代替类型常量。
  • createSlice():接受reducer函数的对象,切片名称和初始状态值,并自动生成切片reducer,并带有相应的动作创建者和动作类型。
  • createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分派动作类型的 thunk
  • createEntityAdapter: 生成一组可重用的 reducer 和 selector 来管理 store 中的规范化数据
  • 重新选择库中的createSelector实用程序,重新导出以方便使用。

回忆一下之前创建 Redux 需要做什么

  1. 定义初始状态(state) const initialState = { value: 0 };
  2. 定义动作(action)const action = () => ({ type: 'counter/incremented' })
  3. 创建 reducer function (state = initialState, action) () { ... }
  4. 配置中间件 Thunk
  5. 配置 Redux DevTools window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f
  6. 创建仓库(store)export default createStore(counterReducer)

看来要要完成 Redux 的搭建还是挺繁杂的,那看看 Redux Toolkit 需要怎么做呢

配置使用 Redux Toolkit

创建 reducer

通过 createSlice 方法可以一键完成之前的前三个步骤

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => { state.value += 1 },
    decrement: state => { state.value -= 1 }
  }
})

export const { increment, decrement } = counterSlice.actions

export default counterSlice.reducer

之前我们讲过在 reducer 函数需要返回一个全新状态,在这里就完全不需要担心这个问题了

创建仓库(store)

通过 configureStore 方法可以一键完成上述步骤的后三个步骤

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from './counter'
export default configureStore({
  reducer: {
    counter: counterReducer
  }
})

组件中使用

除了前面讲述到的两种方式,还可以使用Hooks

  1. 导入 stroe 直接使用
  2. 使用 connect 连接器
  3. 使用 hooks 函数
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment } from "../store/reducer";

export default () => {
  const count = useSelector(state => state.counter.value)
  const dispatch = useDispatch()
  return (
    <div>
      <div>{count}</div>
      <div>
        <button onClick={() => dispatch(increment())}>+</button>
        <button>-</button>
      </div>
    </div>
  )
}

怎么样是不是简单明了许多,但是注意 hooks 函数在 class 组件中是不生效的

更多用法

还有许多其它用法可以参照官方文档Redux Toolkit 使用

最后

本文章大部分案例参照的是 Redux 中文网

还有部分是参照 技术胖的教程