# Redux 简介

# Redux 的工作流程

react

# Redux 的三个基本原则

  • 单一事实来源。Redux 全局只有一个 store,里面包含了唯一的状态对象树;

  • 状态只读。这就是在强调状态的不可变性,只有通过派发 action 的方式才能触发 reducer,返回一个包含变更的新状态;

  • 状态变更不应有副作用。在 store 中使用的 reducer,都必须是不会产生副作用的纯函数。

# Redux 何时用

当我们为项目引入了 Redux 后,并不意味着就与 useState 说再见了。它们可以共存,而且可以配合得很好。

一般情况下可以这样分工:

  • 全局状态倾向于放到 Redux store 里;

  • 局部状态倾向于放到 React state 里;

  • 业务状态倾向于放到 Redux store 里;

  • 交互状态倾向于放到 React state 里;

  • 必要时,可以把外部状态同步到 Redux store 里。

业务状态是指与业务直接相关的状态,这些状态理论上剥离 UI 也可以使用,比如在单元测试中、Node.js 环境中等等。

交互状态(也称作 UI 状态),是与用户交互相关的状态,主要控制着用户与应用的交互过程,用于提升用户体验。

# Redux 核心 API

  • createStore

  • store.dispatch()

  • store.getState()

  • store.subscribe()

# Redux DevTools

  • 这个浏览器扩展可以用来跟踪调试 state 和 action 包含的数据,还提供了一个很酷的时间旅行功能。

  • 可以在 Chorme 网上应用商店里搜索添加这个插件帮助调试 Redux。

  • 默认是不显示插件面板的,如下。点击 the instructions (opens new window) 就会跳转到插件的使用文档。

react

我们需要在代码中创建 store 的时候传入第二个参数,才会在浏览器中显示插件面板。

const store = createStore(
  reducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
1
2
3
4

react

# Redux 中间件

react

Redux 中间件可以让我们提供一个拦截器在 reducer 处理 action 之前被调用。在这个拦截器中,我们可以自由处理获得的 action。无论是把这个 action 直接传递到 reducer,或者构建新的 action 发送到 reducer,都是可以的。

Redux 常见的中间件有 redux-thunk (opens new window)redux-saga (opens new window)

# redux-thunk

1. 和 redux-devtools 一起使用

redux-devtools (opens new window) 其实也是 Redux 的一个中间件,所以可以和 redux-thunk 一起使用。

/*
 * /store/index.js
 */
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer";

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

// 创建 store 的时候把 reducer 传给它
const store = createStore(reducer, enhancer);

export default store;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

2. redux-thunk 的用法

redux-thunk (opens new window) 如果发现接受到的 action 是一个函数,那么就不会传递给 Reducer,而是执行这个函数,并把 dispatch 作为参数传给这个函数,从而在这个函数中我们可以自由决定何时,如何发送 Action。

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

const composedEnhancer = applyMiddleware(thunkMiddleware)
// 在创建 Redux Store 时指定了 redux-thunk 这个中间件
const store = createStore(rootReducer, composedEnhancer)
1
2
3
4
5
6
7
function fetchData() {
  return dispatch => {
    dispatch({ type: 'FETCH_DATA_BEGIN' })
    fetch('./url').then(res => {
      dispatch({ type: 'FETCH_DATA_SUCCESS', data: res})
    }).catch(err => {
      dispatch({ type: 'FETCH_DATA_FAILURE', error: err})
    })
  }
}
1
2
3
4
5
6
7
8
9
10
import fetchData from './fetchData'

function dataList() {
  const dispatch = useDispatch()
  // dispatch 了一个函数由 redux-thunk 中间件去执行
  dispatch(fetchData())
}
1
2
3
4
5
6
7

通过这种方式,我们就实现了异步请求逻辑的重用。这一套结合 redux-thunk 中间件的机制,就称之为异步 Action

因此,异步 Action 并不是一个具体的概念,而可以把它看作是 Redux 的一个使用模式。它通过组合使用同步 Action,在没有引入新概念的同时,用一致的方式提供了处理异步逻辑的方案。

# redux-saga

redux-saga (opens new window) 是 Redux 的另一个常用中间件,它会把异步请求统一放到 sagas.js 文件中进行管理。sagas.js 文件返回的是一个 generator 函数。

1. 和 redux-devtools 一起使用

引入方法跟 redux-thunk 类似,不过需要先建好一个 sagas.js 文件,并在创建 store 之后使用它。

/*
 * /store/index.js
 */

import { createStore, applyMiddleware, compose } from "redux";
import createSagaMiddleware from "redux-saga";
import todoSagas from "./sagas";
import reducer from "./reducer";

const sagaMiddleware = createSagaMiddleware();

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

// 创建 store 的时候把 reducer 传给它
const store = createStore(reducer, enhancer);

sagaMiddleware.run(todoSagas);

export default store;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
 * /store/sagas.js
 */

// 返回一个 generator 函数
function* todoSagas() {}

export default todoSagas;
1
2
3
4
5
6
7
8

2. redux-saga 的用法

redux-saga 有很多 api,所以,它的使用也要比 redux-thunk 复杂的多。

之前 action 只能被 reducer 接收,使用了 redux-saga 之后,action 也能在 sagas.js 文件中被接收到。

/*
 * TodoList.js
 */

import { getInitList } from '../../store/actionCreators';

componentDidMount() {
	const action = getInitList();
	store.dispatch(action);
}
1
2
3
4
5
6
7
8
9
10
/*
 * /store/actionCreators.js
 */

import { GET_INIT_LIST } from './actionTypes';

export const getInitList = () => ({
  type: GET_INIT_LIST
})
1
2
3
4
5
6
7
8
9
/*
 * /store/actionTypes.js
 */

export const GET_INIT_LIST = 'get_init_list';
1
2
3
4
5
/*
 * /store/sagas.js
 */

import { put, takeEvery } from "redux-saga/effects";
import { GET_INIT_LIST } from "./actionTypes.js";
import { initListAction } from "./actionCreators";
import axios from "axios";

function* getInitList() {
  // generator 函数要捕获错误时,需要用 try ... catch
  try {
    const res = yield axios.get("/api/todolist");
    const action = initListAction(res.data.data);
    yield put(action);
  } catch {
    console.log("请求失败");
  }
}

function* todoSagas() {
  yield takeEvery(GET_INIT_LIST, getInitList);
}

export default todoSagas;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# React Redux

我们在 React 中使用 Redux,一般会借助 Redux 官方的 React 连接器 React Redux (opens new window)

这个库把 Redux 的 store 放到了 context 里,但并没有借助 React 的 context 更新机制来响应 store 内部的更新。在早期版本中,React Redux 提供的高阶组件订阅 store 变化,当有变化时调用组件的 forceUpdate() 方法。

而在新版(v8.0)中,高阶组件使用了 React 的新 Hooks API:useSyncExternalStore (opens new window),用这个 Hook 返回的 props 来更新被修饰的组件。

# Provider

Provider 的作用就是把 store 提供给了它所包含的所有组件,使得它们都可以使用。

import { Provider } from "react-redux";
import store from './store';

const App = (
  // Provider 把 store 提供给了它所包含的所有组件,它们都可以使用
  <Provider store={store}>
    <TodoList />
  </Provider>
)

ReactDOM.render(
  App,
  document.getElementById("root"),
  () => {
    console.log("Render successful!");
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# connect

connect 方法是让组件和 store 做连接。

// connect 方法是让 TodoList 和 store 做连接
// TodoList 是一个 UI 组件,当 connect 将它和 store 连接起来之后,返回的结果是一个容器组件
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
1
2
3

# mapStateToProps

mapStateToProps 的作用是将 store 里的数据映射到组件的 props,返回一个对象。

// 将 store 里的数据映射到组件的 props
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue
  }
}
1
2
3
4
5
6

# mapDispatchToProps

mapDispatchToProps 的作用是把 store.dispatch 方法挂载到组件的 props 上,也是返回一个对象。

// 把 store.dispatch 方法挂载到组件的 props 上
const mapDispatchToProps = (dispatch) => {
  return {
    handleInputChange(e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      dispatch(action);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# Redux Toolkit

Redux Toolkit (opens new window) 是 Redux 官方推出的一套更易于使用的封装库 Redux Toolkit,用来简化 Redux 开发。

  • 降低了配置 Redux store 的复杂度;

  • 减少了 Redux 所需的样板代码;

  • 内置了 Redux 必备的扩展库。

Redux Toolkit 新引入了一个概念 slice,即切片。切片是一组相关的 state 默认值、action、reducer 的集合。

首先用 Redux Toolkit 的 createSlice 创建 slice,然后从这个 slice 中拿到生成的 actionCreator 和 reducer,用 configureStore 消费这个 reducer 创建 store。接下来的步骤就与前面 Redux 的例子类似了,有一点区别是这边用于派发的 action 都是调用 actionCreator 创建的。

此外,还有一点需要注意的,Redux Toolkit 的 reducer 中默认启用了 Immer,因此我们可以直接用熟悉的 JS API 来修改状态,框架会帮我们加入 state 的不可变性。

import { createSlice, configureStore } from '@reduxjs/toolkit';

const cardListSlice = createSlice({
  name: 'cardList',
  initialState: [],
  reducers: {
    addCard(state, action) {
      state.unshift(action.payload.newCard);
    },
    removeCard(state, action) {
      const index = state.findIndex(card => card.title === action.payload.title);
      if (index !== -1) {
        state.splice(index, 1);
      }
    }
  }
});
export const { addCard, removeCard } = cardListSlice.actions;

const store = configureStore({
  reducer: cardListSlice.reducer
});
store.subscribe(() => console.log(store.getState()));

store.dispatch(addCard({ newCard: { title: '开发任务-1' } })); // [{ title: '开发任务-1' }]
store.dispatch(addCard({ newCard: { title: '测试任务-2' } })); // [{ title: '测试任务-2' } { title: '开发任务-1' }]
store.dispatch(removeCard({ title: '开发任务-1' })); // [{ title: '测试任务-2' }]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 其他应用状态管理框架

# Recoil

Recoil (opens new window) 是 React 的实验性状态管理库。它提供了几个单独使用 React 难以实现的功能,同时兼容 React 的最新功能。

# Jotai

Jotai (opens new window) 是一个 React 的原始和灵活的状态管理库,它使用受 Recoil 启发的模型采用原子方法进行全局 React 状态管理。

# MobX

MobX (opens new window) 是以透明的函数式响应编程(Transparent Functional Reactive Programming,TFRP)的方式实现状态管理的。

MobX 与 Immer 是同一个作者,甚至比 Immer 还早面世 3 年。因此它也是鼓励用熟悉的 JS 类型和方法修改数据,由框架来界定前后的变更。

# XState

XState (opens new window) 比起 Redux 和 MobX 来说更加硬核一些。它本身就是一个有限状态机的 JS/TS 实现,且遵守了 W3C 的 XCXML 规范。

XState 有一个强项,就是它的可视化状态图 (opens new window)

# Redux 小实战

下面是使用 antd 和 Redux 实现的 todolist 功能。完整代码如下:

# TodoList.js

/*
 * TodoList.js
 */

import React, { Component } from;
import "antd/dist/antd.css";
import { Input, Button, List } from "antd";
import store from "../../store";
// import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM } from '../../store/actionTypes';
import {
  getInputChangeAction,
  getAddItemAction,
  getDeleteItemAction,
} from "../../store/actionCreators";

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = store.getState(); // 获取 store 里的数据
    // console.log(store.getState());
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    store.subscribe(this.handleStoreChange); // 订阅,当 store 发生变化的时候,就会执行里面的函数
  }

  render() {
    return (
      <div style={{ marginTop: "10px", marginLeft: "10px" }}>
        <div>
          <Input
            value={this.state.inputValue}
            placeholder="todo info"
            style={{ width: "300px", marginRight: "10px" }}
            onChange={this.handleInputChange}
          />
          <Button type="primary" onClick={this.handleBtnClick}>
            提交
          </Button>
          <List
            style={{ marginTop: "10px", width: "374px" }}
            bordered
            dataSource={this.state.list}
            renderItem={(item, index) => (
              <List.Item>
                <span>{item}</span>
                <Button
                  type="primary"
                  danger
                  onClick={this.handleDelete.bind(this, index)}
                >
                  删除
                </Button>
              </List.Item>
            )}
          />
        </div>
      </div>
    );
  }

  handleInputChange(e) {
    // const action = {
    // 	type: CHANGE_INPUT_VALUE,
    // 	value: e.target.value
    // }
    const action = getInputChangeAction(e.target.value);
    store.dispatch(action);
  }

  handleStoreChange() {
    // console.log('store change');
    this.setState(store.getState());
  }

  handleBtnClick() {
    // const action = {
    // 	type: ADD_TODO_ITEM
    // }
    const action = getAddItemAction();
    store.dispatch(action);
  }

  handleDelete(idx) {
    // const action = {
    // 	type: DELETE_TODO_ITEM,
    // 	index: idx
    // }
    const action = getDeleteItemAction(idx);
    store.dispatch(action);
  }
}

export default TodoList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

# index.js

/*
 * /store/index.js
 */

import { createStore } from "redux";
import reducer from "./reducer";

// 创建 store 的时候把 reducer 传给它
const store = createStore(
  reducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

export default store;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# reducer.js

/*
 * /store/reducer.js
 */

import {
  CHANGE_INPUT_VALUE,
  ADD_TODO_ITEM,
  DELETE_TODO_ITEM,
} from "./actionTypes";

const defaultState = {
  inputValue: "",
  list: [],
};

// reducer 可以接收 state,但是绝不能修改 state
// 所以我们需要把原来的 state 复制一份出来修改
// reducer 必须是个纯函数
// 纯函数是指,给定固定的输入,就一定会有固定的输出,而且不会有任何副作用
export default (state = defaultState, action) => {
  // console.log(state, action);
  if (action.type === CHANGE_INPUT_VALUE) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputValue = action.value;
    return newState;
  }
  if (action.type === ADD_TODO_ITEM) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.list.push(newState.inputValue);
    newState.inputValue = "";
    return newState;
  }
  if (action.type === DELETE_TODO_ITEM) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.list.splice(action.index, 1);
    return newState;
  }
  return state;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# actionTypes.js

/*
 * /store/actionTypes.js
 */

// 统一管理 action 变量
export const CHANGE_INPUT_VALUE = "change_input_value";
export const ADD_TODO_ITEM = "add_todo_item";
export const DELETE_TODO_ITEM = "delete_todo_item";
1
2
3
4
5
6
7
8

# actionCreators.js

/*
 * /store/actionCreators.js
 */

// 通过 actionCreators 来统一派发 action

import {
  CHANGE_INPUT_VALUE,
  ADD_TODO_ITEM,
  DELETE_TODO_ITEM,
} from "./actionTypes";

export const getInputChangeAction = (value) => ({
  type: CHANGE_INPUT_VALUE,
  value,
});

export const getAddItemAction = () => ({
  type: ADD_TODO_ITEM,
});

export const getDeleteItemAction = (index) => ({
  type: DELETE_TODO_ITEM,
  index,
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

最终效果如下:

react

# 容器组件和 UI 组件的拆分

  • 容器组件又被称为聪明组件,因为容器组件并不管页面最后长什么样,它关注的是整个组件的业务逻辑,主要负责整个组件的功能实现。

  • UI 组件又被称为傻瓜组件,它只负责页面的渲染展示,没有任何业务逻辑可言。

  • 可以把上面做好的 todolist 组件拆成容器组件 TodoList 和 UI 组件 TodoListUI,如下:

/*
 * TodoList.js
 */

import React, { Component } from;
import "antd/dist/antd.css";
import store from "../../store";
import {
  getInputChangeAction,
  getAddItemAction,
  getDeleteItemAction,
} from "../../store/actionCreators";
import TodoListUI from "./TodoListUI";

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = store.getState(); // 获取 store 里的数据
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleDelete = this.handleDelete.bind(this);
    store.subscribe(this.handleStoreChange); // 订阅,当 store 发生变化的时候,就会执行里面的函数
  }

  render() {
    return (
      <TodoListUI
        inputValue={this.state.inputValue}
        list={this.state.list}
        handleInputChange={this.handleInputChange}
        handleBtnClick={this.handleBtnClick}
        handleDelete={this.handleDelete}
      />
    );
  }

  handleInputChange(e) {
    const action = getInputChangeAction(e.target.value);
    store.dispatch(action);
  }

  handleStoreChange() {
    this.setState(store.getState());
  }

  handleBtnClick() {
    const action = getAddItemAction();
    store.dispatch(action);
  }

  handleDelete(idx) {
    const action = getDeleteItemAction(idx);
    store.dispatch(action);
  }
}

export default TodoList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*
 * TodoListUI.js
 */

// UI 组件,只负责渲染

import React, { Component } from;
import { Input, Button, List } from "antd";

class TodoListUI extends Component {
  render() {
    return (
      <div style={{ marginTop: "10px", marginLeft: "10px" }}>
        <div>
          <Input
            value={this.props.inputValue}
            placeholder="todo info"
            style={{ width: "300px", marginRight: "10px" }}
            onChange={this.props.handleInputChange}
          />
          <Button type="primary" onClick={this.props.handleBtnClick}>
            提交
          </Button>
          <List
            style={{ marginTop: "10px", width: "374px" }}
            bordered
            dataSource={this.props.list}
            renderItem={(item, index) => (
              <List.Item>
                <span>{item}</span>
                <Button
                  type="primary"
                  danger
                  onClick={() => {
                    this.props.handleDelete(index);
                  }}
                >
                  删除
                </Button>
              </List.Item>
            )}
          />
        </div>
      </div>
    );
  }
}

export default TodoListUI;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 无状态组件

  • 当一个组件只有一个 render 函数的时候,我们就可以把它改写成一个无状态组件,相对于普通组件来说性能比较高。这是因为无状态组件就是一个函数,普通组件会有各种生命周期要执行,性能肯定没法跟无状态组件相比。

  • 将上面的 UI 组件 TodoListUI 改写成无状态组件如下:

import React from;
import { Input, Button, List } from "antd";

const TodoListUI = (props) => {
  return (
    <div style={{ marginTop: "10px", marginLeft: "10px" }}>
      <div>
        <Input
          value={props.inputValue}
          placeholder="todo info"
          style={{ width: "300px", marginRight: "10px" }}
          onChange={props.handleInputChange}
        />
        <Button type="primary" onClick={props.handleBtnClick}>
          提交
        </Button>
        <List
          style={{ marginTop: "10px", width: "374px" }}
          bordered
          dataSource={props.list}
          renderItem={(item, index) => (
            <List.Item>
              <span>{item}</span>
              <Button
                type="primary"
                danger
                onClick={() => {
                  props.handleDelete(index);
                }}
              >
                删除
              </Button>
            </List.Item>
          )}
        />
      </div>
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
上次更新时间: 2023年12月11日 01:00:18