React

React 状态管理与 Redux、 React Redux

By John Han
Picture of the author
Published on
image alt attribute

React 状态管理与 Redux、 React Redux

React 是一个 UI 库,只能做 UI 相关的工作,它没有架构、没有模板、没有设计模式、没有路由,也没有数据管理。

1. 如何有效管理 React 应用的 state

通常,一个 React 应用是由多个组件组成的,组件是由 UI数据 组成。 通常,UI 部分不需要我们来关心,毕竟 React 本事就是一个 UI 库。

那么,数据就显得格外重要了,尤其是大型 React 应用。

数据可以是简单的,也可以是复杂的。

如果数据比较简单,我们可以直接获取数据后,设置 state 就行了。

伪代码,处理简单数据:

function ComponentA(props) {
  const [dataList] = useEffect(() => {
    fetch("https://XXXX/data", {}).then((err, data) => {
      setState(data);
    });
  }, []);

  return (
    <div className="App">
      <FuncCom></FuncCom>
    </div>
  );
}

export default ComponentA;

假如这是个比较复杂的组件,我们需要获取用户权限、用户手动获取多个来源的数据(如,购物车、优惠券信息等)、支付状态,那么,这时的状态 管理就会比较棘手。

多人协作的任务,如多人编辑的文档。通常需要保存所有用户的所有操作,文档的数据也在不断的更新。这时,状态管理也会比较麻烦。

假如有一个复杂的表单页面(中后台有很多类似的需求),里面有数十个组件,组件之间是需要联动的,例如,用户将付款方式由微信改为支付宝,表单其他的组件可能就需要修改字段、将微信二维码替换为支付宝二维码。大量的联动往往会导致组件的复杂性成倍增加

总结一下就是:

组件的状态很复杂,需要进行特别的管理。

1. 为什么需要 Redux

React 是一个 UI 框架,只能做 UI 相关的工作,它没有架构、没有模板、没有设计模式、没有路由,也没有数据管理。

一个应用肯定是有自己的架构的,如 MVC、MVVM 等。

一个应用中有多种设计模式,如:组件间的通信可以有不同的设计模式,可以看这里

react组件间通信
react组件间通信

Redux 用自己的方式实现了应用的全局状态管理,进而实现组件间的通信等问题

react组件间通信
react组件间通信

遇到如下问题后我们可能需要考虑引入 Redux:

  • 应用中有着相当大量的、随时间变化的数据
  • 应用中的 state 需要有一个单一可靠数据来源
  • 你觉得把所有 state 放在最顶层组件中已经无法满足需要了

2. Redux 要点

    1. 应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。
    1. 改变 state 的惟一办法是触发 action,它是一个描述发生什么的对象。
    1. 为了描述 action 如何改变 state 树,你需要编写 reducers。

项目中的 Redux 架构:

Redux 架构
Redux 架构

3. 何时需要 Redux?

由于上面两个问题,我们可能需要状态管理工具来帮助我们管理 State.

何时需要 Redux?

官方答案

  • 你有大量的应用状态,而这些状态在应用中很多地方都是需要的
  • 应用状态频繁更新
  • 更新该状态的逻辑可能很复杂
  • 该应用有一个中等或大型的代码库,可能会有很多人在工作
  • 你需要看到这个状态是如何随着时间的推移而更新的

一句话总结就是:当应用的 state 变得不可控时,我们需要一个统一的管理器

然而,还有两个回答:

Pete Hunt,一位 React 的早期贡献者:

If you aren't sure if you need it, you don't need it.

Dan Abramov,这家伙厉害了,Redux 作者之一,React 团队核心成员:

Don't use Redux until you have problems with vanilla React.

这两个回答听着有点像劝退的意思 🤔 ,不过从侧面说明了一个问题:使用 Redux 是要付出代价的,用之前要搞清利弊

4. Redux 核心概念

官网截图:

redux介绍
redux介绍

Redux 要做的只有一件事,管理可预见的 State.

为了达到可预见的目的,Redux 引入了三个核心概念,和一系列原则.

4.1 Redux 三个核心概念

  • Store(存放 state tree): 一个存放了应用所有更改的内容的JS 对象

  • Action: 一个 JS 对象

  • Reducer: 一个函数,根据上一个 state 和 action 生成一个新的 state

Redux 三个核心概念工作流程:

react组件间通信
react组件间通信

工作流程描述:

  • 首先有个 Store,用来存储 state
  • state 可以由 Reducer 初始化
  • 然后 UI 组件订阅需要的 state
  • state 的改变只能通过 Reducer 根据 旧的 state 和 Action 来完成,然后生成新的 state
  • UI 组件通过 dispatch 方法发送 action 给 store
  • store 会把 旧的 state 和 Action 发送给 Reducer
  • 红色箭头代表数据传递方向
正是 store、action、reducers 三者按照规则来运行,才保证了 state 的可预测。

4.2 Redux 原则 🌟🌟🌟

本小节根据 Dan 的课程总结而来。

  1. 🌟 应用程序中所有更改的内容(包括数据和 UI 状态)都包含在一个对象中,我们称其为 statestate tree

  2. 🌟 state tree是只读的,不能写和修改

  3. 🌟 想改变state tree只能通过 action,action 是个 JS 对象,以最小的方式描述应用程序中的变化。action 必须有 “type”属性,且其值不为 “undefined”.

  4. 🌟 Reducer 必须是个纯函数(不能有副作用,如请求数据、修改 DOM 等)

5. Redux 有什么用

Redux 统一保存状态数据,在隔离了数据与 UI 的同时,负责处理数据的绑定

redux 工作模型
redux 工作模型

作用

  • 组件间共享数据(state)
  • 某个状态需要在任何地方都可以被随时访问
  • 某个组件需要改变另一个组件的状态的时候

应用场景:

  • 语言切换
  • 黑暗模式切换
  • 用户登录全局数据共享等
  • 全局数据联动

5. 使用 Redux

5.1 简单的使用 Redux

import { createStore } from "redux";

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

// 2. 生成 Store
let store = createStore(counterReducer);

// 3. 定义 Action
let actions = {
  incremented: { type: "counter/incremented" },
  decremented: { type: "counter/decremented" },
};

// 4. dispatch action
store.dispatch(actions.incremented);
store.dispatch({ type: "counter/incremented" });
store.dispatch(actions.decremented);

// 5. 订阅 store,然后做处理(如:更新 UI)
store.subscribe(() => console.log(store.getState())); // store.getState() 获取 state

5.2 严格遵守 Redux 原则

Redux 原则中说了具体要遵守的规则。

为了避免写 Redux 相关业务时忘记遵守相关规则,社区有一些不错的解决方案。

原则 1: state 不可修改

方案:

使用 immutable 定义 state:

import { Map, List } from "immutable";

// 使用
const state = {
  cats: Map({ a: 1, b: 2 }),
  names: List(["John", "kobe"]),
};

function reducer(state = state, action) {
  switch (action.type) {
    catch "":
    break;
    default:
    break;
  }
}

6.3 项目中使用 Redux

项目中的 Redux 架构:

redux 工作模型
redux 工作模型

显然,项目中使用 Redux 比 Demo 中会复杂的多。

例如,上面的简单示例中所有变量都在同一个文件中,真实项目中的文件往往在不同的文件下。

项目中首先要解决的是 store 如何传递给各个组件的问题:

  • 将 store 注入顶级组件中的 props

    缺点:必须逐级传递 store,即使这一层级的组件并没有使用到 store

  • 利用 context 特性,将 store 放在 context 的 value 中

    主流方式,React Redux 有实现该功能(<Provider>)

6.3.1 将 store 注入顶级组件中的 props

项目目录:

redux 入口文件
redux 入口文件

1. 定义 reducer:

import { ChangeLoginStatus } from "./actions";

// 定义初始化状态,为 immutable,不可修改
const defaultState = {
  login: false,
  username: null,
};

const Reducers = (state = defaultState, action) => {
  switch (action.type) {
    case ChangeLoginStatus:
      return { username: action.payload };
    default:
      return state;
  }
};

export default Reducers;

2. 定义 actions:

// 定义action type
export const ChangeLoginStatus = "user/changeLoginStatus";

// 定义 actionCreator 用于创建 action
export const changeLoginStatusActionCreator = (username) => {
  return {
    type: ChangeLoginStatus,
    payload: username,
  };
};

3. 创建 store:

import { createStore, combineReducers } from "redux";
import userReducer from "./user/reducers.js";

const reducers = combineReducers({
  user: userReducer,
});

// 处理后的 reducers 需要作为参数传递在 createStore 中
export const store = createStore(reducers);

4. 将 store 注入项目入口文件 index.js 的 props:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { store } from "./store/store.js";

// 这里注入 store
ReactDOM.render(<App store={store} />, document.getElementById("root"));

5. app.js 中使用 store:

import "./App.css";
import FuncCom from "./components/funcComponent";

function App(props) {
  // 打印看看 store 是什么
  console.log("store:", props.store);
  return (
    <div className="App">
      // 将store传递给组件 FuncCom
      <FuncCom store={pros.store}></FuncCom>
    </div>
  );
}

export default App;

// 输出结果
// store: {
//   "dispatch": f e(n),
//   "getState": f f(),
//   "liftedStore": {},
//   "replaceReducer": ƒ (n),
//   "subscribe": ƒ subscribe(listener)
// }

我们可以得到如下示意图:

redux props 传递 store
redux props 传递 store

说明:

  • 蓝色框代表我们的视图组件
  • 组件之间通过 props 传递 store
  • 左侧的 Store 代表 Redux 管理的 state
  • 绿色箭头代表视图组件和 state 交互(至于如何交互,后面会讨论)

这样有一个很大的问题,就是 必须层层通过 props 传递 store⚠️,哪怕这个组件没有使用 state 也要传递,否则,他的所有子组件都没法使用 state.

6.3.2 利用 Context 跨组件传递 Store

Context 可以解决层层传递的问题。

1. 创建 context 文件:

// context.js
import React from "react";

// 定义 context 初始化数据
const context = {
  user: {
    name: "",
    isLogin: false,
  },
};

// 创建 Context
export const Context = React.createContext(context);

2. 入口文件中使用 Context:

// index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Context } from "./context/context";
import { store } from "./store/store";

// 将 store 赋值给 Context.Provider 的 value
  <Context.Provider value={store}>
    <App />
  </Context.Provider>,
  document.getElementById("root")
);

3. 在子组件中使用 Context 传递的值:

import React, { useState } from "react";
import { Context } from "../context/context";

let Func = () => {
  // Context.Consumer 里的函数获取默认参数 value, value 就是 Context.Provider里提供的 value
  return (
    <Context.Consumer>
      {(store) => (
        <div>
          <p>
            {`name: ${store.getState().user.username} , isLogin: ${
              store.getState().user.login
            }`}
          </p>
        </div>
      )}
    </Context.Consumer>
  );
};

export default Func;

可以看出,使用 Context.Consumer 可以在任意层级的组件中获取到离他最近的Context.Provider提供的 value.

7. Redux 工具

Redux 的”强约束“保障了状态可控,同时,”强约束“也带来了更多模板代码,需要使用工具来提高开发效率。

7.1 React Redux

核心卖点:

  • Redux 团队维护,紧跟 React 和 Redux 更新
  • 以可预测的方式,管理组件state

8. 使用 Redux 的取舍

Redux 官网里写的很清楚了,看这里或许能解决你很多疑惑 👉 Should You Use Redux

舍:

  • 由于其特殊架构(柯里化的函数式编程、State 不可变),需要写很多繁琐的模板代码
  • state 的管理会不太清晰(如:所有的 state 都放在 store,比放在各个组件更难管理)

得:

  • 便于调试(chrome 插件)
  • 极其方便的 Undo(撤销)、Redo(重做)
  • 协作环境
  • 持久化和从状态启动(如视频播放进度等)

9. 手写一个简单的 Redux

明确需求,实现简易的核心功能

  • 使用纯函数
  • createStore() 新建 store,用于存储 state
  • dispatch() 派发 action
  • reducer() 处理 action + state 生成新的 state

思维导图:

redux 思维导图
redux 思维导图

教程推荐

Redux 作者 Dan Abramov 的教程,够权威 -- Fundamentals of Redux Course from Dan Abramov

Youtube 上精简教程 -- Learn Redux from Scratch

参考

Redux 一手资源聚集之地 -- Redux 官网

Render Props -- Redux 官网

高阶组件 -- Redux 官网

深入理解并实现 redux 与 react-redux

深入理解 redux 及其中间件原理

生动形象地介绍了 Redux -- 从设计的角度看 Redux

Redux 入坑进阶-源码解析

手写一个 Redux,深入理解其原理

Stay Tuned

Want to become a Next.js pro?
The best articles, links and news related to web development delivered once a week to your inbox.