# 状态管理

状态管理沉思录从实习期到现在，在公司做了一年的全职前端工程师，一直是在做后台业务系统，日常处理表单逻辑和复杂状态交互，对 react 也算是比较熟悉了。react 提供了组件，一种表述视图、管理状态、对接渲染器、驱动前端应用 run 起来的机制。帮我们很好地解决了从状态到视图的转化关系。但随着所编写的代码逻辑越来越复杂，组件间的交互（上下级的、同级的、跨层级的）渐渐频繁，管理状态的方式也面临挑战。我也曾迷茫过，什么才是最正确的选择，来回踱步、重构代码，一遍一遍。\
react class19年刚进公司实习的时候，react v16已经出现，当时的工作是维护一个老系统，用的是 react v15，系统中也完全没有使用到状态管理库，所以先是稳扎稳打的写了一波 react class 组件。类组件当中，state 字段是组件的状态，使用 setState 方法异步更新这个状态。

1. 年少无知的我也干过直接对 state 赋值状态的蠢事，为什么不可以这样？
2. 为什么组件状态是异步更新而不是同步的呢？这样的意义是什么？
3. DidMount 和 DidUpdate 中 重复的逻辑简直无解。

\
\
&#x20;以下的整理内容会针对示例。

1. 一个计数器组件，有一个 count 状态（由 props.initCount 初始化） 和一个 countMul2（= count \* 2） 的衍生计算状态，多个计数器组件共享同一个 share 状态。
2. 下面有三个按钮，分别是 「+1」、「-1」、「3 秒后 +1」、「置 0」，「共享+1」
3. 「+1」、「-1」、「置 0」 立即生效，但是，「3 秒后 +1」会被前面三个操作终止，同时多次点击「3秒后 +1」只会保留最后一次操作，即防抖。
4. 「共享+1」会将一个共享状态 share + 1。

|![](https://bytedance.feishu.cn/space/api/box/stream/download/asynccode/?code=f9f05b2b8f0bae2bfe93e4a0f3cfb8ae_8f118824ce50c961_boxcnzs5pAY1ssYTJF5ilhsxZPf_BnEwNqrN5rqyP8pD1mvonPSK5RMEPB8N)\
`import React, { useState } from "react";`\
`// 共享状态const SharedCountContext = React.createContext({  shared: 0,  setShared: (s: number) => {}});`\
`// 先写下组件初始状态const initState = {  count: 0,  countMul2: 0};`\
`// 定义 state 类型type StateType = typeof initState;`\
`// 定义props类型interface CounterProps {  initCount?: number;}`\
`// 继承 Componentexport class Counter extends React.Component<CounterProps, StateType> {  // 写下 defaultProps 增加鲁棒性  static defaultProps: CounterProps = {    initCount: 0  };`\
&#x20; `// 注入 context  static contextType = SharedCountContext;  context!: React.ContextType<typeof SharedCountContext>;`\
&#x20; `constructor(props: CounterProps) {    super(props);    // 状态初始化    this.state = Object.assign({}, initState, { count: props.initCount });  }`\
&#x20; `// 引用  timer?: number;`\
&#x20; `computeCount2 = () => {    return this.state.count * 2;  };`\
&#x20; `// 生命周期  componentDidMount() {    // 计算 countMul2    this.setState({      countMul2: this.computeCount2()    });  }`\
&#x20; `componentDidUpdate(prevProps: CounterProps, prevState: StateType) {    if (this.state.count !== prevState.count) {      if (this.timer) { // 取消 timer        clearTimeout(this.timer);        this.timer = void 0;      }      // 计算 countMul2, 与 mount 重复!!!      this.setState({        countMul2: this.computeCount2()      });    }  }`\
&#x20; `// 事件回调  handleInc = () => {    this.setState({ count: this.state.count + 1 });  };`\
&#x20; `handleDec = () => {    this.setState({ count: this.state.count - 1 });  };`\
&#x20; `handleReset = () => {    this.setState({      count: 0    });  };`\
&#x20; `handleAsyncInc = () => {    if (this.timer) {      clearTimeout(this.timer);      this.timer = void 0;    }    this.timer = setTimeout(() => {      this.setState({ count: this.state.count + 1 });    }, 3000);  };`\
&#x20; `// 渲染返回 JSX  render() {    return (      <div>        <div>          <button>count : {this.state.count}</button>          <button>count2 : {this.state.countMul2}</button>          <button>shared : {this.context.shared}</button>        </div>        <div>          <button onClick={this.handleInc}>inc</button>          <button onClick={this.handleDec}>dec</button>          <button onClick={this.handleAsyncInc}>async inc</button>          <button onClick={this.handleReset}>reset</button>          <button            onClick={() => this.context.setShared(this.context.shared + 1)}          >            inc shared          </button>        </div>      </div>    );  }}`\
`export default () => {  // 偷懒写了 hooks  const [shared, setShared] = useState(0);`\
&#x20; `return (    <SharedCountContext.Provider value={{ shared, setShared }}>      <Counter initCount={1} />      <hr />      <Counter initCount={5} />    </SharedCountContext.Provider>  );};`\
react hooks在 react v16 引入 Fibre 之后，hooks 也就出现了，它为函数组件中引入了状态和生命周期，让函数组件拥有了类组件的一些能力，注入、复用逻辑也变得简单（也取代了高阶组件的一些功能）。\


1. 类组件的 state 是一个整体，而 hooks state 是独立的，可以封装出逻辑复用。
2. 重度使用 hooks 在运行时会产生很多闭包，然后又要释放掉对象，性能上不如类组件。
3. useEffect 包含多种生命周期，可以把副作用根据依赖关系拆开写，更清晰、简洁。
4. useMemo 用于生成、缓存计算值，相比类组件可以少一次渲染。
5. useRef 用于引用非 state 对象。

\
`import React, {  useState,  FC,  useMemo,  useContext,  useCallback,  useRef,  useEffect} from "react";`\
`// 共享状态const SharedCountContext = React.createContext({  shared: 0,  setShared: (s: number) => {}});`\
`const Counter: FC<{ initCount: number }> = (props) => {  // 声明 state  const [count, setCount] = useState(props.initCount);`\
&#x20; `// 计算状态  const countMul2 = useMemo(() => count * 2, [count]);`\
&#x20; `// 注入 context  const context = useContext(SharedCountContext);`\
&#x20; `// 引用  const timer = useRef(null as number | null);`\
&#x20; `// 回调  const handleInc = useCallback(() => {    setCount((c) => c + 1);  }, []);`\
&#x20; `const handleDec = useCallback(() => {    setCount((c) => c - 1);  }, []);`\
&#x20; `const handleReset = useCallback(() => {    setCount(0);  }, []);`\
&#x20; `const clearTimer = () => {    if (timer.current) {      clearTimeout(timer.current);      timer.current = null;    }  };`\
&#x20; `// 生命周期  useEffect(() => {    clearTimer();  }, [count]);`\
&#x20; `const handleAsyncInc = useCallback(() => {    clearTimer();    timer.current = setTimeout(() => {      setCount((c) => c + 1);    }, 3000);  }, []);`\
&#x20; `return (    <div>      <div>        <button>count : {count}</button>        <button>count2 : {countMul2}</button>        <button>shared : {context.shared}</button>      </div>      <div>        <button onClick={handleInc}>inc</button>        <button onClick={handleDec}>dec</button>        <button onClick={handleAsyncInc}>async inc</button>        <button onClick={handleReset}>reset</button>        <button onClick={() => context.setShared(this.context.shared + 1)}>          inc shared        </button>      </div>    </div>  );};`\
`export default () => {  const [shared, setShared] = useState(0);`\
&#x20; `return (    <SharedCountContext.Provider value={{ shared, setShared }}>      <Counter initCount={1} />      <hr />      <Counter initCount={5} />    </SharedCountContext.Provider>  );};`\
函数组件 比 类组件少了30行代码。\
react + redux随着引用不断迭代升级，功能完善，状态的传递变得越来越繁重，要么提取到上层容器，层层下传；要么使用 context 维护共享状态。但是，上层容器可能根本就不关心这个状态逻辑，但是却要管理这些状态导致逻辑混乱，层层下传 props 也需要很多工作量。不如将状态从组件中抽离出去，独立管理，组件仅仅订阅这个状态，既可以做逻辑的复用，又能实现状态的共享。redux 是 react 社区首选的状态管理工具。通过构建一个全局唯一数据源（全局的状态），通过 Action 触发 Reducer 函数式更新 State，并通知到 View 的单向数据流，使得整个应用处在一个良性的循环中，状态的变化可以追溯，易于排查问题。|![](https://bytedance.feishu.cn/space/api/box/stream/download/asynccode/?code=f464c543988268bf0e0f3d37e4adf5d2_8f118824ce50c961_boxcnEoN6ilOsRqzB8XYlhc2Zwg_RWpcxFIUv8s6gbQq6ZGyoxjuIqCTqjif)\


1. 实际上 redux 的这种结构与 react 的机制是完全一致的，只是 redux 并不关心 state 被如何使用，只管发布。state 产生 view，用户操作 view 产生 action，action 被回调处理（reducer）更新 state，state 又更新了（发布到） view ……
2. 唯一数据源、状态不可变、函数式更新使得 redux state 成为一个巨大的状态机。
3. 使用 redux 编写逻辑，要严格的拆分状态逻辑和视图组件，要写很多的样板代码（ActionType、ActionCreator、Reducer），写多了很恶心，但是十分规范、可扩展。
4. redux 本身并不直接支持复杂异步逻辑，可以加上中间件来实现（redux-thunk、redux-saga、redux-observable），还有一些上层库，像rematch、@reduxjs/toolkit 来简化 redux 代码。
5. redux 并不适用于所有的情况，应当仅仅用在单例状态上（否则请使用 hook 复用逻辑、封装组件）。
6. 结合 redux-devtool-extension 、HMR 可以利用浏览器插件来观察 action、state 随时间的变化、可在时间线中恢复状态，方便复现、调试问题。

\
react + mobx mobx 在几个状态管理库中独树一帜。使用了代理对象的方式，做到响应式，内部的原理与 vue 的响应式机制是一样的。当一个对象被 mobx 装饰生成，其属性就拥有了响应式的特点。结合 mobx-react 用于监听状态的变化，可以写出非常简洁直观的 react 代码，相比于原生 react hook 和 redux，代码更容易做细致的优化。\


1. 声明 observable 对象有三种方式，observable 函数（单例对象）、observable 装饰器（对类）、useLocalStore hook（组件内部声明）。
2. 使用 mobx-react 中的 observer 包装组件即可为组件注入相应 observable 对象的能力。
3. mobx-react 中的 \<Observer> 组件可以局部监听，不用重新 render 整个组件。
4. @observable.ref 可以只追踪引用，不将整个属性对象变成代理
5. 在类 constructor 中使用 reaction 方法可以做出类似 useEffect 的功能

\
`const store = observable({  a: 1,  inc() {    this.a++;  }});`\
`class Store {  constructor(){    reaction(()=>this.a, a=>console.log(a)); // 监听 a 属性，打印  }  @observable a: number = 1;  @action setA(a) {    this.a = a;  }  @computed get b() {    return this.a * this.a;  }}`\
`export default function App() {  return useObserver(() => (    <div className="App">      <div>{store.a}</div>      <div>        <button onClick={() => store.inc()}>inc </button>      </div>    </div>  ));}`\
react + hooks + unstated-next将自定义 hook 转换为共享的状态容器。使用 Provider 提供 context，useContainer 注入 context，相当方便。\
`import React, { useState } from "react"import { createContainer } from "unstated-next"import { render } from "react-dom"`\
`function useCounter(initialState = 0) {  let [count, setCount] = useState(initialState)  let decrement = () => setCount(count - 1)  let increment = () => setCount(count + 1)  return { count, decrement, increment }}`\
`let Counter = createContainer(useCounter)`\
`function CounterDisplay() {  let counter = Counter.useContainer()  return (    <div>      <button onClick={counter.decrement}>-</button>      <span>{counter.count}</span>      <button onClick={counter.increment}>+</button>    </div>  )}`\
`export default ()=>{    return <Counter.Provider>        <CounterDisplay />        <CounterDisplay />    </Counter.Provider>}`\
react + rxjs  = all in onerxjs 是 angular 框架的基石。redux 官网有一句话，如果你已经使用了 rxjs ，那么就不需要使用 redux，因为 redux 可以通过 rxjs 实现，并且可以使用 rxjs 的 operators 实现许多 redux 中间件的功能。这里有人分享过 [YouTube 视频](https://www.youtube.com/watch?v=vd5S_7Qm9WE\&list=PL37ZVnwpeshEGvbeADo0HKaaTCsC7fk1x\&index=35) 。\
在 npm上 rxjs 的下载量是 redux 的三倍，rxjs 不只是用在了前端状态管理，它是通用的数据流框架。\
使用 rxjs 创建出来的是流，是数据的转换过程，所以当我一开始想使用 react + rxjs 时，摸不着头脑，需要一定的封装才能利用起来，可以使用上面视频链接的思路实现一个 redux。\
我个人不是很喜欢使用 redux 这种严谨又繁重的方案，所以写了一个简单的 hook 帮助我整合 react 和 rxjs 的逻辑，也就是下面的方案。\
react + rxjs + hooks + unstated-nextrxjs 很灵活，但是在 react 中手动管理 rxjs 对象是比较麻烦的，也是我之前一直搞不明白怎么用的原因。希望使用的 rxjs 能力其实在其强大的 operator 上，只要抽象、封装好状态，对接上 rxjs 的流，就能处理很灵活的场景了。\
一直都想着做一个强大的状态组件出来，内置各种骚操作，但是事实上这很不现实，组件会越来越膨胀直到无法理解，每次我尝试写一个这样的东西出来，都会因为心智负担过重，ts 泛型类型怼不上，思路中断，最后放弃。\
我在使用了各种状态工具之后，对状态和数据流有一定的了解后，脑海中的轮廓也渐渐清晰，编写了一个 hook，封装了 rxjs 的状态流（监测状态，并使用 operator 去做一些事情，只要自己定制几个 operator 就能复用逻辑，这就是最灵活的做法——无招胜有招。）同时提供足够强大、灵活的能力，方便调试，内部使用 rxjs 灵活的流（可以复用 rxjs 的各种 operators 实现复杂的数据流逻辑）来代替 react 的 useState 和 useEffect，结合 unstated-next 可以将此状态逻辑变成共享的，将局部状态和全局状态统一起来，可以快速的迁移逻辑。[sandbox示例](https://codesandbox.io/s/distracted-ritchie-nvz5s?file=/src/components/RxBox/index.tsx)。\
**使用这个 useRxBox 的原则是：**

1. props、state 、derivation 会合并在一起，在 routine 方法和 useRxBox 返回中提供。

*
  * props 不会被 setState 覆盖，传入什么就是什么（主要是为了在 routine 统一处理副作用流）。
  * state 用作交互状态 以及 事件信号。
  * derivation 用作计算值 以及 异步请求返回结果。

1. &#x20;useRxBox 返回的 setState 只能设置 state 部分（props 不应该被设置，derivation 由 routine 流去控制），这个用 ts 类型限制了，但是无视 ts 的情况下也能设置 derivation。routine 中的 setState 在 ts 类型上允许设置 derivation（这也是设计的结果）。

\
Hook 代码（代码在上面 sandbox 是一样的）：`import { Observable, BehaviorSubject, of } from "rxjs";import {  mergeMap,  map,  distinctUntilChanged,  catchError,  tap,  debounceTime,  share,  pairwise,  filter,  take,} from "rxjs/operators";import React, { useEffect, useState, useCallback, useRef } from "react";import _ from "lodash";import { notification } from "antd";`\
`// 用于获得差异对象const getChange = <A extends Record<string, any> = {}, B extends Record<string, any> = {}>(  oldV: A,  newV: B,): any =>  _(newV)    .keys()    .filter((k) => oldV[k] !== newV[k])    .map((key) => [key, { from: oldV[key], to: newV[key] }])    .fromPairs()    .value();`\
`// 操作符：用于选择一个观察值export const select = <T, R>(mapper?: (a: T) => R) => (source: Observable<T>): Observable<R> =>  source.pipe(map(mapper || _.identity), distinctUntilChanged(_.isEqual));`\
`// 操作符：用于简化设置属性用的export const emitMap = <T, R>(  mapper: (a: T) => R = _.identity,  receiver: (b: R) => any = _.noop,) => (source: Observable<T>): Observable<T> => source.pipe(tap((a) => receiver(mapper(a))));`\
``const logAction = (debug?: boolean, name?: string, actionName?: string) => {  if (debug) {    console.log(`[rxbox ${name || ""}] action : ${actionName}`);  }};``\
`// 副作用存入的函数类型export interface RoutineType<S, U, D> {  (api: {    name?: string;    state$: Observable<S & U & D>;    setState: (newState: Partial<S & D>) => void;  }): Observable<any>;}`\
`export interface RxBoxProps<S extends object = {}, U extends object = {}, D extends object = {}> {  name?: string; // 逻辑名称  debug?: boolean; // 调试模式，方便查看变化  props?: U; // props  state?: S; // 初始状态，可以在 routine 和 mergeState 方法中设置，用于交互状态  derivations?: D; // 衍生状态，可以在 routine 方法中设置，由于 计算属性 和 异步数据  routines?: ([string, RoutineType<S, U, D>] | RoutineType<S, U, D>)[]; // 传入 routines}`\
`export const useRxBox = <S extends object = {}, U extends object = {}, D extends object = {}>(  props: RxBoxProps<S, U, D> = {} as RxBoxProps<S, U, D>,) => {  type UnionState = S & U & D;`\
&#x20; `const [state, setState] = useState(    () => _.assign({}, props.state, props.derivations, props.props) as UnionState,    // 初始化合并 状态  );`\
&#x20; `// 创建 rxjs 流  const [state$] = useState(() => new BehaviorSubject<UnionState>(state));`\
&#x20; `useEffect(() => {    // 初始化 state$ 流    const stateDebounce$ = state$.pipe(debounceTime(50), share());    const stateSub = stateDebounce$.subscribe(setState); // 经过 debounce 之后逻辑被充分处理，通知组件更新 state`\
&#x20;   ``// 处理 debug 逻辑    const logSub = stateDebounce$      .pipe(        props.debug ? map((e) => e) : take(0),        pairwise(),        map(([oldv, newv]) => getChange(oldv, newv)),        filter((change) => _.keys(change).length > 0),      )      .subscribe((change) => {        if (props.debug) {          console.log(`[rxbox ${props.name || ""}] change log :`, change);          notification.open({            message: `[rxbox ${props.name || ""}] change log :`,            description: <pre>{JSON.stringify(change, null, 1)}</pre>,          });        }      });``\
&#x20;   `return () => {      // 卸载时取消订阅      stateSub.unsubscribe();      logSub.unsubscribe();    };    // eslint-disable-next-line react-hooks/exhaustive-deps  }, []);`\
&#x20; `const propRef = useRef<U>();  propRef.current = props.props;`\
&#x20; `const mergeState = useCallback(    (partialState: Partial<S>) => {      const newState = _.assign({}, state$.value, partialState, propRef.current);      if (!_.isEqual(newState, state$.value)) {        state$.next(newState);      }    },    // eslint-disable-next-line react-hooks/exhaustive-deps    [setState],  );`\
&#x20; ``useEffect(() => {    // 启动 routine effect    const sub = of(...(props.routines || []))      .pipe(        mergeMap((r, idx) =>          _.isFunction(r)            ? r({ state$, setState: mergeState }).pipe(                tap(() => logAction(props.debug, props.name, `[${idx}]`)),              )            : r[1]({ state$, setState: mergeState, name: r[0] }).pipe(                tap(() => logAction(props.debug, props.name, r[0])),              ),        ),        catchError((e) => {          console.log(e);          return of(null);        }),      )      .subscribe(_.noop);    return sub.unsubscribe.bind(sub);    // eslint-disable-next-line react-hooks/exhaustive-deps  }, []);``\
&#x20; `useEffect(() => {    // 更新 props 部分，与 传入值 保存一致    const newState = _.assign({}, state$.value, propRef.current);    if (!_.isEqual(state$.value, newState)) {      state$.next(newState);    }    // eslint-disable-next-line react-hooks/exhaustive-deps  }, [propRef.current]);`\
&#x20; `return { state, mergeState };};`\
`// 直接可以使用的 组件export function RxBox<S extends object = {}, U extends object = {}, D extends object = {}>(  props: RxBoxProps<S, U, D> & {    children?: (api: {      state: S & U & D;      mergeState: (s: Partial<S>) => void;    }) => JSX.Element | null;  },) {  const { state, mergeState } = useRxBox(props);`\
&#x20; `return props.children?.({ state, mergeState }) || null;}`\
示例：`import { RxBoxProps, select, emitMap, useRxBox } from ".";import { switchMap, delay } from "rxjs/operators";import { of } from "rxjs";import { createContainer, Container } from "unstated-next";import React from "react";`\
`const initState = { count: 0, count2: 1 };`\
`const initDerivation = {  countMul2: 0,  countPlusCount2: 0,  countAsyncMul3: 0 as string | number,};`\
`const sharedConfig: RxBoxProps<typeof initState, {}, typeof initDerivation> = {  state: initState,  derivations: initDerivation,  routines: [    ({ state$, setState }) =>      state$.pipe(        // 计算属性        select((s) => s.count),        emitMap((count) => ({ countMul2: count * 2 }), setState),      ),    ({ state$, setState }) =>      state$.pipe(        // 计算属性        select((s) => ({ a: s.count, b: s.count2 })),        emitMap(({ a, b }) => ({ countPlusCount2: a + b }), setState),      ),    ({ state$, setState }) =>      state$.pipe(        // 异步数据逻辑        select((s) => ({ a: s.count, b: s.countMul2 })),        emitMap(() => ({ countAsyncMul3: "loading..." }), setState),        switchMap((n) => of(n).pipe(delay(1000))),        emitMap(({ a, b }) => ({ countAsyncMul3: a + b }), setState),      ),  ],};`\
`const Shared = createContainer(() => useRxBox(sharedConfig));`\
`export type ValueInContainer<C> = C extends Container<infer T> ? T : any;`\
`const CountConsumer = React.memo(() => {  const { state, mergeState } = Shared.useContainer();  return (    <div>      <div>count : {state.count}</div>      <div>count2 : {state.count2}</div>      <div>countAsyncMul3 : {state.countAsyncMul3}</div>      <div>countPlusCount2 : {state.countPlusCount2}</div>      <div>        <button          onClick={mergeState.bind(null, {            count: state.count + 1,          })}        >          inc count        </button>        <button          onClick={mergeState.bind(null, {            count: state.count - 1,          })}        >          dec count        </button>      </div>      <div>        <button          onClick={mergeState.bind(null, {            count2: state.count2 + 1,          })}        >          inc count2        </button>        <button          onClick={mergeState.bind(null, {            count2: state.count2 - 1,          })}        >          dec count2        </button>      </div>    </div>  );});`\
`export default () => {  const { state, mergeState } = useRxBox(sharedConfig);  return (    <>      <div>        <div>count : {state.count}</div>        <div>count2 : {state.count2}</div>        <div>countMul2 : {state.countMul2}</div>        <div>countAsyncMul3 : {state.countAsyncMul3}</div>        <div>          <button            onClick={mergeState.bind(null, {              count: state.count + 1,            })}          >            inc count          </button>          <button            onClick={mergeState.bind(null, {              count: state.count - 1,            })}          >            dec count          </button>        </div>      </div>      <hr />      下面两个共享状态      <Shared.Provider>        <hr />        <CountConsumer />        <hr />        <CountConsumer />      </Shared.Provider>    </>  );};`\
\
\
