什么是状态管理

状态

  • 状态是表示组件当前状况的 JS 对象。在 React 中,可以使用 useState 或者 this.state 维护组件内部状态,通过 props 传递给子组件使用。

  • 为了避免状态传递过程中出现混乱,React 引入了“单向数据流”的理念。主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时他们会使用接收到的新值,而不是修改已有的值。当组件的更新机制触发后,他们只是使用新值进行重新渲染。

  • 父子组件通信可以直接使用 props 和回调方式;深层次、远距离组件则要通过“状态提升”和 props 层层传递。

常见模式

  • 状态提升:兄弟组件间是没法直接共享状态的,可以通过将状态提升到最近的祖先组件中,所有兄弟组件就可以通过 props 一级级传递获取状态。

  • 状态组合:某些状态可能只在应用程序的特定子树中需要。最好将状态存储在尽可能接近实际需要的位置,这有助于优化渲染行为,使 React 组件树变得更容易调试。

  • 属性下钻:将父组件的状态以属性的形式一级级显示传递给嵌套子组件。

  • ProviderReact Context 通过 Provider 包裹组件,被包裹的所有嵌套子组件都可以不用通过属性下钻而是通过 context 直接获取状态。

层层传递的 value onChange 会对一个优质代码库带来的毁灭性影响,粗暴地把数据塞在 redux 中也并不能让一个应用得到很好的拓展性和可维护性。

要解决的问题

  • 从组件树的「任何地方」读取存储的状态
  • 写入存储状态的能力
  • 提供「优化渲染」的机制
  • 提供「优化内存使用」的机制
  • 与「并发模式的兼容性」
  • 数据的「持久化」
  • 「上下文丢失」问题
  • 「props 失效」问题
  • 「孤儿」问题

心智模型

  • 不可变状态模型
  • 可变状态模型
  • 主要好处是可以使用原生 JS 方法
  • 基于 Proxy 的状态管理的一个缺点是状态不可预测,难以 debug

因为 React 没有官方的状态管理方案,React 生态中状态管理库,百花齐放,演进出很多设计思想和心智模式。如何选择状态管理库就变得十分令人抓狂。

React Context

  • React Context: 在多级嵌套组件场景下,使用“属性下钻”方式进行组件通信是一件成本极高的事情。为了解决这个问题,React 官方提供 Context 用于避免一级级属性传递。

context 的问题

  • Context 存在的问题也是老生常谈。在  react  里,context  是个反模式的东西,不同于  redux  等的细粒度响应式更新,context 的值一旦变化,所有依赖该 context 的组件全部都会  force update,因为  context API  并不能细粒度地分析某个组件依赖了 context 里的哪个属性,并且它可以穿透  React.memo  和  shouldComponentUpdate  的对比,把所有涉事组件强制刷新。

  • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。这种对于一个组件树而言是“全局”的数据,往往会被多个组件所需要,但是使用 props 传递又会比较麻烦,这就是 Context 出现的背景。

综上,在系统中跟业务相关、会频繁变动的数据在共享时,应谨慎使用 context

如果决定使用 context,可以在一些场景中,将多个子组件依赖的不同 context属性提升到一个父组件中,由父组件订阅 context 并以 prop 的方式下发,这样可以使用子组件的 memo、shouldComponentUpdate 生效。

优点

  • 作为 React 内置的 hook,不需要引入第三方库。
  • 书写还算方便。

缺点

  • Context 只能存储单一值,当数据量大起来时,你可能需要使用createContext创建大量context
  • 直接使用的话,会有一定的性能问题:每一次对state的某个值变更,都会导致其他使用该 state 的组件re-render,即使没有使用该值。 你可以通过useMemo来解决这个问题,但是就需要一定的成本来定制一个通用的解决方案。
  • 无法处理异步请求。对于异步的逻辑,Context API并没有提供任何 API,需要自己做封装。 无法处理数据间的联动。Context API并没有提供API来生成派生状态,同样也需要自行去封装一些方法来实现。

代码示例

// hooks 方式
import React, { createContext, useState, useContext } from 'react'

const CountContext = createContext()

function Counter() {
  const count = useContext(CountContext)
  return <h2>{count}</h2>
}

function Example() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <CountContext.Provider value={count}>
        <Counter />
      </CountContext.Provider>
    </div>
  )
}

// class 方式

import React, { createContext, Component } from 'react'

const CountContext = createContext()

class Counter extends Component {
  render() {
    return (
      <CountContext.Consumer>
        {(count) => <h2>{count}</h2>}
      </CountContext.Consumer>
    )
  }
}

class Example extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0,
    }
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
        <CountContext.Provider value={this.state.count}>
          <Counter />
        </CountContext.Provider>
      </div>
    )
  }
}

React 外部状态管理库

  • React 的外部状态管理库一直以来是 React 生态中非常内卷的一个领域。目前比较常见的状态管理库有 Redux(包括基于 ReduxDvaIcestore)、MobxZustandRecoilJotaiValtioHox 等。

  • Redux  作为  React  状态管理的老大哥,下载量上依然遥遥领先其他库。Mobx  作为往年热度仅次于  Redux  的状态管理库,位置正逐步有被  zustand  超越的趋势。recoil/jotai/valtio 作为这两年热门的新兴库热度也在逐步上升。hox  则处于不温不火的尴尬地位。

  • 以  React v16.8  版本为分水岭,状态管理库可分为  Class  时代和  Hooks  时代。Class  时代中  Redux 和  Mobx  都是非常优秀的状态库。随着  Hooks  时代的到来,状态管理的心智模型也逐步发生着演变。整体呈现从中心化到去中心化,从单一状态到原子状态,从  Provider  到拥抱  Hooks  等演变趋势。

class 时代

reduxopen in new window

  • Redux  的灵感来源于  Flux  架构和函数式编程原理,状态更新可预测、可跟踪,提倡使用「单一存储」。这通常会「导致将所有的东西存储在一个大的单体存储中」。将 UI 和远程实体状态之间的所有东西都放在一个地方管理,这变得非常难以管理。对性能造成了不小的压力。

  • 单向数据流:Redux  应用中数据的生命周期遵循以下 4 个步骤:

    • 调用  store.dispatch(action)
    • Redux  store 调用传入的 reducer 函数。
    • 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
    • Redux  store 保存了根 reducer 返回的完整 state 树。

由此可看出 Redux 遵循“单向数据流”和“不可变状态模型”的设计思想。这使得 Redux 的状态变化是可预测、可调式的

  • 三大原则

    • 单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
    • State 是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
    • 使用纯函数来执行修改:为了描述 action 如何改变 state tree ,你需要编写 reducers。
  • 如何处理异步

    • redux 没有规定如何处理异步数据流,最原始的方式就是使用 Action Creators,也就是在制造 action 之前进行各种的异步操作,你可以把要复用的操作抽离出来。

    • 当然这样并不优雅,在实际项目中我们通常使用类似redux-thunkredux-saga这些中间件来支持处理异步。

  • 如何处理数据间联动

    • redux  并没有提供派生状态的方法,需要自行封装。
    • react-redux useSelector 获取状态后,你可以编写一些逻辑来处理派生状态。如果派生状态需要复用,记得给抽离出来。
  • 缺点

    • 陡峭的学习曲线。将副作用扔给中间件来处理,导致社区一堆中间件,学习成本陡然增加。比如处理异步请求的 Redux-saga、计算衍生状态的 reselect
    • 大量的样板代码。每个 action 都需要编写一个 action type,每个 reducer 都需要编写一个 switch case,每个组件都需要编写 mapStateToPropsmapDispatchToProps,这些样板代码让人疲惫。
    • 过于中心化。Reduxstore 是全局唯一的,所有的状态都放在一个 store 中,这导致了 Redux 的状态管理是过于中心化的。当应用复杂度上升时,Reduxstore 会变得越来越臃肿,这会导致性能问题。
    • reducer 要返回新的对象,如果更新的值层级较深,更新成本也很高。
    • 更多的内存占用,由于采用单一数据源,所有状态存储在一个 state 中,当某些状态不再需要使用时,也不会被垃圾回收释放内存。

    当然,redux 也在致力于解决上述缺点。比如,redux toolkitopen in new window就旨在让开发者使用标准方式编写 redux 逻辑。主要解决 redux 的 3 个问题

    • 配置 redux store 过于麻烦
    • 必须手动额外添加很多包才能正常使用 redux
    • redux 需要太多模板代码

    不过,即使有 redux toolkit 的加持,redux 的学习成本依旧不低

    • 代码示例
    
    // store.js
    import { createStore } from 'redux'
    import reducer from './reducer'
    
    const store = createStore(reducer)
    
    export default store
    
    // reducer.js
    
    const defaultState = {
      inputValue: '',
      list: []
    }
    
    export default (state = defaultState, 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
    }
    
    // TodoList.js
    
    import React, { Component } from 'react'
    
    import store from './store'
    
    class TodoList extends Component {
      constructor(props) {
        super(props)
        // 从 store 中获取数据
        this.state = store.getState()
        this.handleInputChange = this.handleInputChange.bind(this)
        this.handleStoreChange = this.handleStoreChange.bind(this)
        this.handleBtnClick = this.handleBtnClick.bind(this)
        // 订阅 store 的改变
        store.subscribe(this.handleStoreChange)
      }
    
      render() {
        return (
          <div>
            <div>
              <input
                value={this.state.inputValue}
                onChange={this.handleInputChange}
              />
              <button onClick={this.handleBtnClick}>提交</button>
            </div>
            <ul>
              {this.state.list.map((item, index) => {
                return (
                  <li
                    key={index}
                    onClick={this.handleItemDelete.bind(this, index)}
                  >
                    {item}
                  </li>
                )
              })}
            </ul>
          </div>
        )
      }
    
      handleInputChange(e) {
        const action = {
          type: 'change_input_value',
          value: e.target.value
        }
        store.dispatch(action)
      }
    
      handleStoreChange() {
        this.setState(store.getState())
      }
    
      handleBtnClick() {
        const action = {
          type: 'add_todo_item'
        }
        store.dispatch(action)
      }
    
      handleItemDelete(index) {
        const action = {
          type: 'delete_todo_item',
          index
        }
        store.dispatch(action)
      }
    }
    
    export default TodoList
    
    
    
    // 使用 Redux Toolkit 重构
    // store/todoSlice.js
    import { createSlice } from '@reduxjs/toolkit'
    
    const todoSlice = createSlice({
      name: 'todo',
      initialState: {
        inputValue: '',
        list: []
      },
      reducers: {
        changeInputValue(state, action) {
          state.inputValue = action.payload
        },
        addTodoItem(state) {
          state.list.push(state.inputValue)
          state.inputValue = ''
        },
        deleteTodoItem(state, action) {
          state.list.splice(action.payload, 1)
        }
      }
    })
    
    export const { changeInputValue, addTodoItem, deleteTodoItem } =
      todoSlice.actions
    
    export default todoSlice.reducer
    
    // store/index.js
    
    import { configureStore } from '@reduxjs/toolkit'
    
    import todoReducer from './todoSlice'
    
    const store = configureStore({
      reducer: {
        todoReducer
      }
    })
    
    export default store
    
    
    // TodoList.js 类组件中使用
    import React, { Component } from 'react'
    import { connect } from 'react-redux'
    
    import {
      changeInputValue,
      addTodoItem,
      deleteTodoItem
    } from './store/todoSlice'
    
    class TodoList extends Component {
      render() {
        const { inputValue, list, changeInputValue, addTodoItem, deleteTodoItem } =
          this.props
        return (
          <div>
            <div>
              <input value={inputValue} onChange={changeInputValue} />
              <button onClick={addTodoItem}>提交</button>
            </div>
            <ul>
              {list.map((item, index) => {
                return (
                  <li key={index} onClick={() => deleteTodoItem(index)}>
                    {item}
                  </li>
                )
              })}
            </ul>
          </div>
        )
      }
    }
    
    const mapStateToProps = (state) => {
      return {
        inputValue: state.todoReducer.inputValue,
        list: state.todoReducer.list
      }
    }
    
    const mapDispatchToProps = (dispatch) => {
      return {
        changeInputValue(e) {
          dispatch(changeInputValue(e.target.value))
        },
        addTodoItem() {
          dispatch(addTodoItem())
        },
        deleteTodoItem(index) {
          dispatch(deleteTodoItem(index))
        }
      }
    }
    
    export default connect(mapStateToProps, mapDispatchToProps)(TodoList)
    
    
    // 函数式组件使用 Redux Toolkit
    
    import React from 'react'
    
    import { useSelector, useDispatch } from 'react-redux'
    
    import {
      changeInputValue,
      addTodoItem,
      deleteTodoItem
    } from './store/todoSlice'
    
    const TodoList = () => {
      const inputValue = useSelector((state) => state.todoReducer.inputValue)
      const list = useSelector((state) => state.todoReducer.list)
      const dispatch = useDispatch()
    
      return (
        <div>
          <div>
            <input
              value={inputValue}
              onChange={(e) => dispatch(changeInputValue(e.target.value))}
            />
            <button onClick={() => dispatch(addTodoItem())}>提交</button>
          </div>
          <ul>
            {list.map((item, index) => {
              return (
                <li key={index} onClick={() => dispatch(deleteTodoItem(index))}>
                  {item}
                </li>
              )
            })}
          </ul>
        </div>
      )
    }
    
    export default TodoList
    
    // 与 typescript 结合使用
    // store/todoSlice.ts
    
    import { createSlice, PayloadAction,createAsyncThunk  } from '@reduxjs/toolkit'
    
    interface TodoState {
      inputValue: string
      list: string[]
    }
    
    
    // 异步
    export const fetchUsers = createAsyncThunk(
      'users/fetchUsers',
      async (userId, thunkAPI) => {
        const response = await userAPI.fetchById(userId)
        return response.data
      }
    )
    
    const todoSlice = createSlice({
      name: 'todo',
      initialState: {
        inputValue: '',
        list: []
      } as TodoState,
      reducers: {
        changeInputValue(state, action: PayloadAction<string>) {
          state.inputValue = action.payload
        },
        addTodoItem(state) {
          state.list.push(state.inputValue)
          state.inputValue = ''
        },
        deleteTodoItem(state, action: PayloadAction<number>) {
          state.list.splice(action.payload, 1)
        }
      },
       extraReducers: {
        // Add reducers for additional action types here, and handle loading state as needed
         builder.addCase(fetchUsers.pending, (state, action) => {
           state.status = 'loading'
       }),
        builder.addCase(fetchUsers.fulfilled, (state, action) => {
            state.status = 'idle'
            // Add any fetched posts to the array
            state.list = action.payload
        }),
        builder.addCase(fetchUsers.rejected, (state, action) => {
            state.status = 'failed'
            state.error = action.error.message
        })
      }
    })
    
    export const { changeInputValue, addTodoItem, deleteTodoItem } =
      todoSlice.actions
    
    export default todoSlice.reducer
    
    // store/index.ts
    
    import { configureStore } from '@reduxjs/toolkit'
    
    import todoReducer from './todoSlice'
    
    const store = configureStore({
      reducer: {
        todoReducer
      }
    })
    
    export default store
    
    // TodoList.tsx
    
    import React, { useEffect } from 'react'
    import { useSelector, useDispatch } from 'react-redux'
    
    import {
      changeInputValue,
      addTodoItem,
      deleteTodoItem,
      fetchUsers
    } from './store/todoSlice'
    
    const TodoList = () => {
      const inputValue = useSelector((state) => state.todoReducer.inputValue)
      const list = useSelector((state) => state.todoReducer.list)
      const dispatch = useDispatch()
    
      useEffect(() => {
        dispatch(fetchUsers())
      }, [dispatch])
    
      return (
        <div>
          <div>
            <input
              value={inputValue}
              onChange={(e) => dispatch(changeInputValue(e.target.value))}
            />
            <button onClick={() => dispatch(addTodoItem())}>提交</button>
          </div>
          <ul>
            {list.map((item, index) => {
              return (
                <li key={index} onClick={() => dispatch(deleteTodoItem(index))}>
                  {item}
                </li>
              )
            })}
          </ul>
        </div>
      )
    }
    
    export default TodoList
    
    

Dvaopen in new window

  • dva 首先是一个基于 reduxredux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-routerfetch,所以也可以理解为一个轻量级的应用框架。

  • Dva 的特点:

    • 易学易用,仅有 6 个 api,对 redux 开发者尤其友好,配合 umi 使用后更是降低为 0 api。
    • elm 概念,通过 reducers, effectssubscriptions 组织 model
    • 插件机制,比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoadinghideLoading
    • 支持 HMR,基于 babel-plugin-dva-hmr 实现 componentsroutesmodelsHMR

Dva 大幅降低了 Redux 的上手成本,过去也在社区拥有了拥趸,github star 数 16.1k。不过,从 2019.11 开始就没有新的版本发布,看起来已经处于不维护状态。

// src/models/todo.js
export default {
  namespace: 'todo',
  state: {
    inputValue: '',
    list: [],
  },
  reducers: {
    changeInputValue(state, action) {
      return {
        ...state,
        inputValue: action.payload,
      }
    },
    addTodoItem(state) {
      return {
        ...state,
        list: [...state.list, state.inputValue],
        inputValue: '',
      }
    },
    deleteTodoItem(state, action) {
      const list = [...state.list]
      list.splice(action.payload, 1)
      return {
        ...state,
        list,
      }
    },
  },
  effects: {
    // 用于处理异步逻辑
    *asyncAddTodoItem(action, { put, call }) {
      yield call(delay, 1000)
      yield put({ type: 'addTodoItem' })
    },
  },
}

function delay(timeout) {
  return new Promise((resolve) => {
    setTimeout(resolve, timeout)
  })
}


// src/pages/todo.js
import React from 'react'
import { connect } from 'dva'

const TodoList = (props) => {
  const { inputValue, list, dispatch } = props

  return (
    <div>
      <div>
        <input
          value={inputValue}
          onChange={(e) =>
            dispatch({ type: 'todo/changeInputValue', payload: e.target.value })
          }
        />
        <button onClick={() => dispatch({ type: 'todo/addTodoItem' })}>
          提交
        </button>
      </div>
      <ul>
        {list.map((item, index) => {
          return (
            <li
              key={index}
              onClick={() => dispatch({ type: 'todo/deleteTodoItem', payload: index })}
            >
              {item}
            </li>
          )
        })}
      </ul>
    </div>
  )
}

const mapStateToProps = (state) => {
  return {
    inputValue: state.todo.inputValue,
    list: state.todo.list,
  }
}

export default connect(mapStateToProps)(TodoList)


// src/pages/index.js
import React from 'react'
import TodoList from './todo'

const IndexPage = () => {
  return (
    <div>
      <TodoList />
    </div>
  )
}

export default IndexPage

// class 组件中使用
import React, { Component } from 'react'
import { connect } from 'dva'

class TodoList extends Component {
  render() {
    const { inputValue, list, dispatch } = this.props

    return (
      <div>
        <div>
          <input
            value={inputValue}
            onChange={(e) =>
              dispatch({ type: 'todo/changeInputValue', payload: e.target.value })
            }
          />
          <button onClick={() => dispatch({ type: 'todo/addTodoItem' })}>
            提交
          </button>
        </div>
        <ul>
          {list.map((item, index) => {
            return (
              <li
                key={index}
                onClick={() =>
                  dispatch({ type: 'todo/deleteTodoItem', payload: index })
                }
              >
                {item}
              </li>
            )
          })}
        </ul>
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputValue: state.todo.inputValue,
    list: state.todo.list,
  }
}

export default connect(mapStateToProps)(TodoList)


// 与 typescript 结合使用
// src/models/todo.ts
import { delay } from '@/utils/utils'

interface TodoState {
  inputValue: string
  list: string[]
}

interface TodoModelType {
  namespace: 'todo'
  state: TodoState
  reducers: {
    changeInputValue: Reducer<TodoState>
    addTodoItem: Reducer<TodoState>
    deleteTodoItem: Reducer<TodoState>
  }
  effects: {
    asyncAddTodoItem: Effect
  }
}

const TodoModel: TodoModelType = {
  namespace: 'todo',
  state: {
    inputValue: '',
    list: [],
  },
  reducers: {
    changeInputValue(state, action) {
      return {
        ...state,
        inputValue: action.payload,
      }
    },
    addTodoItem(state) {
      return {
        ...state,
        list: [...state.list, state.inputValue],
        inputValue: '',
      }
    },
    deleteTodoItem(state, action) {
      const list = [...state.list]
      list.splice(action.payload, 1)
      return {
        ...state,
        list,
      }
    },
  },
  effects: {
    // 用于处理异步逻辑
    *asyncAddTodoItem(action, { put, call }) {
      yield call(delay, 1000)
      yield put({ type: 'addTodoItem' })
    },
  },
}



// src/pages/todo.tsx
import React from 'react'
import { connect, DispatchProp } from 'dva'
import { ConnectState } from '@/models/connect'

interface TodoListProps extends DispatchProp {
  inputValue: string
  list: string[]
}

const TodoList: React.FC<TodoListProps> = (props) => {
  const { inputValue, list, dispatch } = props

  return (
    <div>
      <div>
        <input
          value={inputValue}
          onChange={(e) =>
            dispatch({ type: 'todo/changeInputValue', payload: e.target.value })
          }
        />
        <button onClick={() => dispatch({ type: 'todo/addTodoItem' })}>
          提交
        </button>
      </div>
      <ul>
        {list.map((item, index) => {
          return (
            <li
              key={index}
              onClick={() => dispatch({ type: 'todo/deleteTodoItem', payload: index })}
            >
              {item}
            </li>
          )
        })}
      </ul>
    </div>
  )
}

icestoreopen in new window

  • icestore 是 IceJs 内置状态管理库。icestore 是面向 React 应用的、简单友好的状态管理方案。

  • 特点:

    • 简单、熟悉的 API:不需要额外的学习成本,只需要了解 React Hooks,对 Redux 用户友好。
    • 集成异步处理:记录异步操作时的执行状态,简化视图中对于等待或错误的处理逻辑。
    • 支持组件 Class 写法:友好的兼容策略可以让老项目享受轻量状态管理的乐趣。
    • 良好的 TypeScript 支持:提供完整的 TypeScript 类型定义,在 VS Code 中能获得完整的类型检查和推断。

icestore  的灵感来自于  rematch 和  constate。整体实现和  rematch  基本一致。rematch  是一个没有模板代码的  redux  最佳实践。icestore  整体配置简单,解决了  redux  学习成本高、大量模板代码等问题,同时又很好的支持了异步处理、TypeScript  和  SSR

  • 代码示例
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, createModel } from '@ice/store'

const delay = (time) =>
  new Promise((resolve) => setTimeout(() => resolve(), time))

// 1️⃣ 使用模型定义你的状态
const counter = createModel({
  state: 0,
  reducers: {
    increment: (prevState) => prevState + 1,
    decrement: (prevState) => prevState - 1,
  },
  effects: () => ({
    async asyncDecrement() {
      await delay(1000)
      this.decrement()
    },
  }),
})

const models = {
  counter,
}

// 2️⃣ 创建 Store
const store = createStore(models)

// 3️⃣ 消费模型
const { useModel } = store
function Counter() {
  const [count, dispatchers] = useModel('counter')
  const { increment, asyncDecrement } = dispatchers
  return (
    <div>
      <span>{count}</span>
      <button type='button' onClick={increment}>
        +
      </button>
      <button type='button' onClick={asyncDecrement}>
        -
      </button>
    </div>
  )
}

// 4️⃣ 绑定视图
const { Provider } = store
function App() {
  return (
    <Provider>
      <Counter />
    </Provider>
  )
}

const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)

mobxopen in new window

  • 设计思想

    • MobX 的主要思想是用「函数响应式编程」和「可变状态模型」使得状态管理变得简单和可扩展。
    • MobX 背后的哲学很简单:
      • 任何源自应用状态的东西都应该自动地获得。其中包括 UI、数据序列化、服务器通讯,等等。
    • React 和 MobX 是一对强力组合。React 通过提供机制把应用状态转换为可渲染组件树并对其进行渲染。而 MobX 提供机制来存储和更新应用状态供 React 使用。
    • 对于应用开发中的常见问题,React  和  MobX  都提供了最优和独特的解决方案。React  提供了优化 UI 渲染的机制,  这种机制就是通过使用虚拟 DOM 来减少昂贵的 DOM 变化的数量。MobX  提供了优化应用状态与  React  组件同步的机制,这种机制就是使用响应式虚拟依赖状态图表,它只有在真正需要的时候才更新并且永远保持是最新的。
  • 心智模型

    • Mobx 的心智模型和 react 很像,它区分了应用程序的三个概:
      • 状态(state):任何影响应用程序的东西都可以被认为是状态。状态可以是简单的原始值,也可以是复杂的对象和数组。
      • 动作 (actions):动作是改变状态的任何东西。它们是纯函数,不能有副作用。
      • 派生 (derivation):派生是从状态和动作中衍生出的任何东西。它们是自动更新的,因此可以保持最新。

    Mobx 虽然心智模型像 react,但是实现却是完完全全的 vue:mutable + proxy(为了兼容性,proxy 实际上使用 Object.defineProperty 实现)。所以,Mobx 本质上就是一个更繁琐的 Vue。

尤大本人也盖过章:React + MobX 本质上就是一个更繁琐的 Vue

  • 代码示例
import React from 'react'
import ReactDOM from 'react-dom'

import { observable, action } from 'mobx'

class Store {
  //@observable: 将普通的数据属性转换成可观察的属性
  @observable count = 0

  // @action: 将普通的方法转换成可以修改状态的行为
  @action.bound
  increment() {
    this.count++
  }

  @action.bound
  decrement() {
    this.count--
  }

  // @computed: 将普通的 getter 方法转换成可以动态获取数据的属性
  @computed
  get doubleCount() {
    return this.count * 2
  }

  //  异步 action 通过 async/await 来实现
  @action.bound
  async asyncDecrement() {
    await delay(1000)
    this.decrement()
  }

  // 通过 autorun 来实现
  // autorun: 在被其观察的任意一个值发生改变时重新执行一个函数。
  constructor() {
    autorun(() => {
      console.log(this.count)
    })
  }

  // 通过 reaction 来实现
  // reaction: 在所选的任一数据发生改变时重新执行一个副作用。
  // constructor() {
  //   reaction(
  //     () => this.count,
  //     (count) => {
  //       console.log(count)
  //     }
  //   )
  // }

  // 通过 when 来实现
  // when: 一旦一个 observable 条件为真就立即执行一次副作用函数。
  // constructor() {
  //   when(
  //     () => this.count > 10,
  //     () => {
  //       console.log('count > 10')
  //     }
  //   )
  // }

  // 通过 flow 来实现
  // flow: 对 MobX 友好的 async/await 替代品,支持取消。
  // @flow
  // *asyncDecrement() {
  //   yield delay(1000)
  //   this.decrement()
  // }

  // 通过 transaction 来实现
  // transaction: Transaction 是底层 API。推荐改用 action 或 runInAction。
  // @action.bound
  // async asyncDecrement() {
  //   await delay(1000)
  //   this.decrement()
  // }
}

const store = new Store()

function Counter() {
  return (
    <div>
      <span>{store.count}</span>
      <button type='button' onClick={store.increment}>
        +
      </button>
      <button type='button' onClick={store.decrement}>
        -
      </button>
    </div>
  )
}

const rootElement = document.getElementById('root')

ReactDOM.render(<Counter />, rootElement)
  • Mobx vs redux

Mobx 和 Redux 的对比,实际上可以归结为 面向对象 vs 函数式和 Mutable vs Immutable。

  • 相比于 redux 的广播遍历 dispatch,然后遍历判断引用来决定组件是否更新,mobx 基于 proxy 可以精确收集依赖、局部更新组件(类似 vue),理论上会有更好的性能,但 redux 认为这可能不是一个问题。

  • Mobx 因为数据只有一份引用,没有回溯能力,不像 redux 每次更新都相当于打了一个快照,调试时搭配 redux-logger 这样的中间件,可以很直观地看到数据流变化历史。

  • Mobx 的学习成本更低,没有全家桶。

  • Mobx 在更新 state 中深层嵌套属性时更方便,直接赋值就好了,redux 则需要更新所有途经层级的引用(当然搭配 immer 也不麻烦)。

  • 优点

    • 代码量少,学习成本低
    • 代码结构简单,易于维护
  • 缺点

    • 依赖装饰器,需要额外配置 babel。
    • 难以调试。由于采用可变状态模型,状态不可预测和追溯,难以 debug。
    • 由于 Mobx 采用可变状态模型,所以需要额外的工具来保证状态的不可变性,比如使用 mobx-state-tree
    • 响应式是基于 Proxy 实现的,希望传递的是一个数组,拿到的却是一个 Proxy。排查问题时有点痛苦。

hooks 时代

Recoilopen in new window

  • 简介

    • Recoil 是在 React Europe 2020 Conference 上 facebook 官方推出的专为 react 打造的状态管理库,动机是解决 react 状态共享模式的局限性。

      • 以往只能将 state 提升到公共祖先来实现状态共享,并且一旦这么做了,基本就无法将组件树的顶层(state 必须存在的地方)与叶子组件 (使用 state 的地方) 进行代码分割。
      • Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合。
  • 设计思想

    • Recoil 的状态集是一个有向图 (directed graph),正交且天然连结于 React 组件树。状态的变化从该图的顶点(atom)开始,流经纯函数 (selector) 再传入组件。
    • Recoil 定义了一个有向图 (directed graph),正交同时又天然连结于你的 React 树上。状态的变化从该图的顶点(我们称之为 atom)开始,流经纯函数 (我们称之为 selector) 再传入组件。基于这样的实现。
      • atom 是状态的基本单位,它是一个可读写的状态容器,可以被任意组件订阅,当 atom 的值发生变化时,所有订阅该 atom 的组件都会被触发重新渲染。
      • selector 是一个纯函数,它接收一个或多个 atom 作为输入,返回一个新的状态。selector 可以被其他 selector 订阅,当它的输入 atom 发生变化时,selector 会被重新计算,然后触发订阅它的组件重新渲染。
      • selectorFamily 是一个函数,它接收一个函数作为参数,返回一个 selector。selectorFamily 可以根据传入的参数动态生成 selector,这样就可以根据参数的不同来订阅不同的 selector。

正交:相互独立,相互间不可替代,并且可以组合起来实现其它功能 Recoil 每一次状态变更都会生成一个不可变的快照,利用这个特性,可以快速实现应用导航相关的功能,例如状态回溯、跳转等。

  • 代码示例
import React from 'react'
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil'

const textState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: '', // default value (aka initial value)
})

const charCountState = selector({
  key: 'charCountState', // unique ID (with respect to other atoms/selectors)
  get: ({ get }) => {
    const text = get(textState)

    return text.length
  },
})

function CharacterCount() {
  const [text, setText] = useRecoilState(textState)
  const count = useRecoilValue(charCountState)

  const onChange = (event) => {
    setText(event.target.value)
  }

  return (
    <div>
      <input type='text' value={text} onChange={onChange} />
      <br />
      Echo: {text}
      <br />
      Character Count: {count}
    </div>
  )
}

export default CharacterCount

// 与 typescript 结合使用
import React from 'react'
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil'

const textState =
  atom <
  string >
  {
    key: 'textState', // unique ID (with respect to other atoms/selectors)
    default: '', // default value (aka initial value)
  }

const charCountState = selector({
  key: 'charCountState', // unique ID (with respect to other atoms/selectors)
  get: ({ get }) => {
    const text = get(textState)

    return text.length
  },
})

const CharacterCount: React.FC = () => {
  const [text, setText] = useRecoilState(textState)
  const count = useRecoilValue(charCountState)

  const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setText(event.target.value)
  }

  return (
    <div>
      <input type='text' value={text} onChange={onChange} />
      <br />
      Echo: {text}
      <br />
      Character Count: {count}
    </div>
  )
}

Jotaiopen in new window

  • 简介

    • Jotai 是由 pmndrs 团队开发的状态管理库,它的设计思想与 Recoil 类似,都是基于原子(atom)和选择器(selector)的状态管理模式,但是 Jotai 的实现更加简单,代码量更少,而且不依赖 Context,所以性能更好。
    • jotai  是一个小型全局状态管理库,它模仿了  useState、useReducer。jotai  有个叫做  atom  的概念,用于表示小的状态片段。和  zustand  不同的是,他是一个组件级别的状态管理库。和  zustand  相同的是同样都基于不可变状态模型。
    • jotai 是 Context 和订阅机制的结合,是面向 React 的一种全局状态管理库。如果你的需求是一个没有额外重复渲染的 Context,那 jotai 是个不错的选择。
  • 特点:

    • 语法简单
    • 性能好
    • jotai 的状态不是全局状态
  • atom  可以在  React  组件的生命周期里创建和销毁。这通过多个  Context  是无法实现的,因为使用  Context  增加一个新的  state  意味着增加一个新的  Provider  组件,如果新增一个组件,它所有的子组件都会被重新挂载,会失去所有状态。

  • 推荐场景:组件为中心的应用,不需要全局状态,但是需要共享状态。

  • 代码示例


import React from 'react'
import { atom, useAtom } from 'jotai'

const countAtom = atom(0)

function Counter() {
  const [count, setCount] = useAtom(countAtom)

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>increment</button>
    </div>
  )
}

export default Counter

// 与 typescript 结合使用

import React from 'react'
import { atom, useAtom } from 'jotai'

const countAtom = atom < number > (0)

const Counter: React.FC = () => {
  const [count, setCount] = useAtom(countAtom)

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>increment</button>
    </div>
  )
}

export default Counter

Zustandopen in new window

  • zustand 是一个轻量级状态管理库,和 redux 一样都是基于不可变状态模型和单向数据流的,状态对象 state 不可被修改,只能被替换。渲染优化要手动通过 selectors 进行。

  • 特点:

    • 轻量级
    • 中心化,单一 store
    • 不可变状态模型
    • 不固执,很少限制,可以和其他库结合使用
  • Zustand vs Redux

    • zustand 和 redux 是非常像的,都基于不可变状态模型,都基于单向数据流。
    • 不过,redux 需要应用被 Context Provider 包裹,zustand 则不需要。
    • 二者更新数据的方式不同,redux 基于 reducers,更新状态的 reducers 是严格的方法,这就使得状态更加可预测。zustand 不使用 reducers 而是通过更灵活的方法来更新状态。
  • 代码示例


import React from 'react'
import create from 'zustand'

const useStore = create((set) => ({
  count: 1,
  inc: () => set((state) => ({ count: state.count + 1 })),
  dec: () => set((state) => ({ count: state.count - 1 })),
}))


function Counter() {
  const count = useStore((state) => state.count)
  const inc = useStore((state) => state.inc)
  const dec = useStore((state) => state.dec)

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={inc}>increment</button>
      <button onClick={dec}>decrement</button>
    </div>
  )
}

export default Counter

// 与 typescript 结合使用

import React from 'react'
import create from 'zustand'

interface State {
  count: number
  inc: () => void
  dec: () => void
}

const useStore = create < State > ((set) => ({
  count: 1,
  inc: () => set((state) => ({ count: state.count + 1 })),
  dec: () => set((state) => ({ count: state.count - 1 })),
}))

const Counter: React.FC = () => {
  const count = useStore((state) => state.count)
  const inc = useStore((state) => state.inc)
  const dec = useStore((state) => state.dec)

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={inc}>increment</button>
      <button onClick={dec}>decrement</button>
    </div>
  )
}


export default Counter

Valtioopen in new window

  • 简介

    • 基于可变状态模型,利用 Proxy 获取一个和 React 集成在一起的不可变快照。
    • 利用 Proxy 自动进行重新渲染优化,这个过程使用了状态使用跟踪技术。通过状态使用跟踪,可以检测到状态的哪部分被使用,让组件实现按使用重新渲染。同时,开发者也可以编写更少的代码。
    • Valtio 也可以和其他库结合使用,比如 React Router、Redux、Mobx 等。
  • 代码示例

import React from 'react'
import { proxy, useSnapshot } from 'valtio'

const state = proxy({
  count: 1,
})

function Counter() {
  const snap = useSnapshot(state)

  return (
    <div>
      <p>count: {snap.count}</p>
      <button onClick={() => (state.count += 1)}>increment</button>
    </div>
  )
}

export default Counter

// 与 typescript 结合使用

import React from 'react'

import { proxy, useSnapshot } from 'valtio'

interface State {
  count: number;
}

const state =
  proxy <
  State >
  {
    count: 1,
  }

const Counter: React.FC = () => {
  const snap = useSnapshot(state)

  return (
    <div>
      <p>count: {snap.count}</p>
      <button onClick={() => (state.count += 1)}>increment</button>
    </div>
  )
}

Hoxopen in new window

  • 简介

    • Hox 是一个基于 React Hooks 的状态管理库,它的 API 设计和 Redux 非常相似,但是使用起来更加简单。
  • 特点

    • 基于 React Hooks
    • API 设计和 Redux 非常相似
    • 使用起来更加简单
    • 与其他库结合使用
    • 优秀的性能和 TypeScript 支持
    • 同时支持局部状态和全局状态,在灵活和简单之间做到了很好的平衡
  • 代码示例


import React from 'react'
import { createModel } from 'hox'

function useCounter() {
  const [count, setCount] = React.useState(0)
  const decrement = () => setCount((count) => count - 1)
  const increment = () => setCount((count) => count + 1)
  return { count, decrement, increment }
}

const useCounterModel = createModel(useCounter)

function Counter() {
  const { count, decrement, increment } = useCounterModel()

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
    </div>
  )
}

export default Counter

// 与 typescript 结合使用

import React from 'react'

import { createModel } from 'hox'

interface State {
  count: number;
  decrement: () => void;
  increment: () => void;
}

function useCounter() {
  const [count, setCount] = React.useState(0)
  const decrement = () => setCount((count) => count - 1)
  const increment = () => setCount((count) => count + 1)
  return { count, decrement, increment } as State
}

const useCounterModel = createModel(useCounter)

const Counter: React.FC = () => {
  const { count, decrement, increment } = useCounterModel()

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
    </div>
  )
}

Ressoopen in new window

  • 简介

    • Resso 最简单的 React 状态管理器。
  • 特点

    • 简单易用
    • 非常聪明
    • 非常小巧
  • 安装

pnpm add resso
# or
yarn add resso
# or
npm i resso
  • 代码示例
import resso from 'resso'

const store = resso({ count: 0, text: 'hello' })

function App() {
  const { count } = store // UI 中 data 须在顶层先解构 🥷
  return (
    <>
      {count}
      <button onClick={() => ++store.count}>+</button>
    </>
  )
}
  • API

    • 初始化
    import resso from 'resso'
    
    const store = resso({
      count: 0,
      inc: () => {
        const { count } = store // 方法中的 data 须在顶层先解构,同样 🥷
      },
    })
    
    • 更新
    // 更新单个 → 直接赋值
    store.count = count + 1
    
    // 更新单个 → 更新函数
    store('count', (prev) => prev + 1)
    
    // 更新多个
    Object.assign(store, { a, b, c })
    
    • 使用
    // UI 中 data 须在顶层先解构,因为它们是以 useState 注入的
    function App() {
      const { count } = store // 须在最顶层,否则将有 React 报错 (Hooks 规则)
    }
    

总结

  • 以上就是我对于 React 状态管理库的一些总结,希望对你有所帮助。
  • 简单场景使用原生的 useState、useReducer、useContext 就能满足;还可以用 Hox 这样小而美的库将 hook 的状态直接拓展成持久化状态,几乎没有额外的心智负担。
  • 复杂场景的应用,redux、mobx 都是经受过千锤百炼的库,社区生态也很完备。
  • Redux 高度模板化、分层化,职责划分清晰,塑造了其状态在可回溯、可维护性方面的优势;搭配 thunk、saga 这些中间件几乎是无所不能。
  • Mobx 的优势是写法简单和高性能,但状态的可维护性不如 redux,在并发模式中的兼容性也有待观察。
  • 随着 hook 和有官方背景的 recoil 的出现,状态管理似乎在朝原子化、组件化的方向发展,这也符合 react 的组件化哲学。Redux 的暴力遍历和分发或许已经是逆潮流的解法。

但是,状态管理库的选择,还是要根据项目的实际情况来选择,没有最好的,只有最适合的。

参考