实名反对 Redux,因为我有更好的 immutable 状态管理器

研究状态管理也算不短时间,对状态管理的本质也渐渐有了自己的认识。在状态管理器领域,有两大流派,即以 redux 为代表的 immutable 流派和以 mobx 为代表的 mutable 流派。从数据流的纯粹性讲,我们更喜欢 immutable,但从写代码的便捷性讲,我们更喜欢 mutable。基于这种主观上的意愿,immer 这个工具诞生了。它以极简主义的方式,为我们提供了以 mutable 的形式修改,但得到一个 immutable 的结果的状态操作形式,从代码量上极速降低工程复杂度。基于 immer,我撰写了新的 react 全局状态管理器 react-immut,源码只有 150 行左右,却完成了 redux, react-redux 的所有工作,甚至你不需要再考虑 redux-thunk 这类用于异步操作的第三方库。

和 react-redux 几乎一致的接口

react-redux 已经被反复证明是最被 react 开发者熟知的状态管理器连接工具,因此我所采用的方式和 react-redux 几乎一模一样。让我们来看看它的最直接的使用方法:

import { createStore, Provider } from 'react-immut'

const store = createStore({
  name: 'timy',
  age: 10,
})

export function App() {
  return (
    <Provider store={store}>
      ...
    </Provider>
  )
}

上面就是我们整个应用的骨架代码。和 redux 完全不同,我们不需要写一大堆 reducer 代码,而是只需要将初始状态传入即可,正是因为如此,我们的代码量瞬间少了无数行。

在具体组件中,我们也使用 connect 进行连接。

import { connect } from 'react-immut'

const mapStateToProps = (state) => {
  const { name, age } = state
  return { name, age }
}

const mapDispatchToProps = (dispatch) => {
  // 这里的 dispatch 就和 react-redux 完全不同了
  const changeName = (name) => dispatch('name', name)
  const changeAge = (age) => dispatch('age', age)
  // 和 react-redux 不同,react-immut 不会主动注入 dispatch 到组件中
  return { changeName, changeAge }
}

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)

function MyComponent(props) {
  const { name, age, changeName, changeAge } = props
  // ...
}

到目前为止从代码层面,几乎极容易理解,没有任何难度。

神奇的 dispatch 方法

和 redux 通过 dispatch action 不同,react-immut 的 dispatch 是一个神奇的方法,它可以让我们实现极简的编程体验。上文红色代码已经见过了 dispatch 的方便之处。接下来,我们将会看到一些神奇的事要发生。

> dispatch(keyPath, value)

这里的 keyPath 可不是单纯的属性名,而是一个属性路径。我们来看看具体用法:

dispatch('parent.child', 'new value')
dispatch('books[0].title', 'new title')
dispatch(['root', symbol], Symbol())

你看,它的变化形式真是多样。

> dispatch(keyPath, prev => next?)

第二个参数是一个函数,它接收当前值,返回新值。这就好玩了,我们可以发挥很多想法了。

dispatch('name', name => {
  return name === 'tomy' ? name : 'gage'
})
dispatch('some', some => {
  some.child.name = 'some'
  some.child.age = 10
})

不一样的事情终于发生了。这里,红色的代码显得格外刺眼,如果在传统的 immutable 系统中,我们是不允许这样写的,但是,在 react-immut 中,它是最好的写法。试着想想,如果放到以前,为了 immutable,你可能必须这样操作:

dispatch('some', some => {
  return {
    ...some,
    child: {
      ...some.child,
      name: 'some',
      age: 10,
    },
  }
})

看,多么麻烦。如果再多几层,混杂对象和数组,那会是令人抓狂的一个操作。但是,基于 immer,我们只需要像前面的示例代码一样写,直接修改对象属性值,就可以了,而且,最终,你得到的状态是 immutable 的,这个操作不会带来对象被修改的结果,放心使用吧。

> dispatch(prev => next?)

不传入 keyPath,那么可以直接获得整个应用的 state。

dispatch(state => {
  state.name = 'tomy'
})

是否返回 next 是很关键的,如果返回 next,那么会用 next 替换 prev 的地位,但是必须注意,next 和 prev 的数据类型必须一直,都是 'object' (对象或数组)。而 immer 的最佳实践,反而是不返回任何内容,不返回内容,恰恰可以让我们更清楚知道,哪些值被修改。

使用 Hooks

都 2020 了,hooks 当然不能少。使用 hooks 方法 useStore 可以省略 connect 的步骤,简化代码。

import { useStore } from 'react-immut'

function MyComponent() {
  const [state, dispatch] = useStore() // 获得整个应用的 state 和 dispatch 方法
}

需要注意的是,useStore 基于 context,也就是说,你必须在 Provider 内部使用 useStore。这样获取全局状态,完全不需要 connect 了,是不是感觉飞起来呢?

Combined Store

很多情况下,我们常常会对我们的状态进行分区,每一个独立分区,我们称之为 namespace。我们倾向于将关于一个业务组件的所有代码文件放在同一个文件夹下面组织,这样有利于我们更好的管理同一个业务组件相关的所有代码。为了方便这种场景,我们提供了一种 combined store,通过 namespace 的方式组织全局状态。

// components/a-some/store.js
export const state = { // state 是必须的
  name: 'Tom',
  age: 10,
}

export function changeName(dispatch, name) { // 定义一些方法
  dispatch(state => { // 这个 state 指向的是这个局部 state,而非全局 state,也就是说,在该文件内,只能修改该命名空间内的 state,而无法修改其他空间的 state
    state.name = name
  })
}

export function changeAge(dispatch, age) {
  dispatch(state => {
    state.age = age
  })
}

接下来,我们使用这个定义好的文件

// app.js
import { createStore } from 'react-immut'
import * as Asome from './components/a-some/store.js'

// 传入第二个参数,第二个参数的结构如下
const store = createStore(null, {
  Asome, // 使用 'Asome' 作为命名空间名称
  // 这里可以引入其他命名空间
})

export default function App() {
  return (
    <Provider store={store}>
      ...
    </Provider>
  )
}

在具体的组件中,通过 key 读取命名空间(本质上和从全局 state 上读取没有差别)。

function MyComponent() {
  const [{ name, age }, { changeName, changeAge }] = useStore('Asome')
  // ...
}

combine 本质上干了两件事,一是把结构化的对象解析出来创建 store,二是解析出来命名空间上的方法,把这些方法添加到 dispatch 上。虽然 dispatch 是一个函数,但是由于在 js 语言中,函数的本质也是对象,所以,可以给函数添加属性,这样,当使用 combined store 之后,可以在这个函数上增加命名空间属性,让开发者可以方便的读取定义好的方法。

const mapDispatchToProps = (dispatch) => {
  const { changeName, changeAge } = dispatch.Asome
  return { changeName, changeAge }
}

异步

和 redux 最大的不同在于,我们没有 reducer,因此我们对 state 的操作都是外部实现的,而非 store 内部定义的。只要我们能调用到 dispatch,我们就可以修改 state。因此,异步操作实在是太简单了。

export function saveName(dispatch, name) {
  fetch('...', { method: 'POST', body: JSON.stringify({ name }) })
    .then(res => res.json())
    .then(data => {
      dispatch(state => {
        state.name = data.name
      })
    })
}

无论在组件内部,还是在 connect 的 mapDispatchToProps 函数中,还是在命名空间定义的方法中,我们都可以调用 dispatch 来实现这个效果。

结束语

本文介绍了 react-immut 的用法,它利用 immer 的超能力,实现极其方便的全局状态更新,设计上又和 react-redux 相同,让开发者可以没有太大的门槛切换。而且,它的源码只有 150 行左右,没有复杂的魔法,踏踏实实写的,你可以在这里读到它的源码。利用 immutable 的特性,我们可以做任何 redux 能做的事情,例如时间旅行等。不再需要样板代码,我们为什么还非得要 redux 呢?

2020-09-04 236

为价值买单

本文价值2.36RMB