mobx-react@5 文档翻译

2,309 阅读11分钟

前言

最近的项目里打算使用 mobx 进行状态管理,逛了一圈似乎没发现官方的中文文档(mobx 有中译而 mobx-react 没有),为了写得顺畅以及少踩坑就直接把整篇文档给翻译了。当时想着就算撞车了也权当自己巩固一下,还能作为对照供人参考,收获一些修改意见也不错。

没想到就在翻译接近完成的时候官方把 mobx-react 的站点下线重定向到了 mobx.js.org,而 React 相关的文档被移入了 React integration。这才知道前几天 mobx 刚刚发布了 6.0.0 版本,先前的一些 API 也发生了变动,像是关键的 hook useLocalStoreuseLocalObservable 替代,具体的变动还是参考官方的 [迁移指南](mobx.js.org/migrating-f…

最后还是决定把翻译大概校对一下发出来,毕竟 mobx 总体的思想不会变化,之后有机会的话还想翻译一下新版的文档。另外本人水平有限,译文如果出现疏漏错译之类的问题欢迎指正。

如何管理状态?

状态管理一直是关于 React 的热门话题,时不时就会有人提出全新的范式。MobX 简化了状态管理的工作并提供了多种使用方法。

创建状态

MobX 中的状态通过 observable object 来表现,具体你可以参考文中提到的方法。

对于需要高健壮性的进阶状态管理,更好的选择是使用 mobx-state-tree。

这里我们使用全新的 useLocalStore 的 hook 来开始旅程(在类组件中是不可用的)。

import { observable } from 'mobx'
import { useLocalStore } from 'mobx-react' // 6.x or mobx-react-lite@1.4.0
function CreatingState() {
  const simpleState = React.useRef(observable.array([1, 2, 3])).current
  const [bigState] = React.useState(createExpensiveStore)
  const localState = useLocalStore(() => ({
    count: 0,
    inc() {
      localState.count += 1
    },
  }))
  return <Rendering simple={simpleState} big={bigState} local={localState} />
}
class CreatingState extends React.PureComponent {
  // or use constructor if class properties are not available for you
  simpleState = observable.array([1, 2, 3])
  render() {
    // class component does not support any other way of
    // keeping observable state within a component
    return <Rendering simple={this.simpleState} />
  }
}

温馨提示,请勿使用 React.useMemo 来保存对状态对象的引用。否则 React 可能会随机抛弃这个对象,同时你也就会丢失数据。

访问状态

在组件之间传递状态是另一个重点内容,这部分的逻辑与 MobX 不相关,但在这里仍然值得一提。

对于简单的使用场景,你可以在组件内创建一个本地的 observable 然后手动通过 prop 来传递(如下所示)。这就和使用 useReducer 之类的方法差不多,除了你只需传递一个单独的对象。

如果需要更健壮的状态管理机制,推荐 使用 React Context 来进行传统的注入。

全局变量状态

在全局变量中保存应用级状态并让组件文件引入,这样的做法能应用于大部分情形。

然而编写测试对于应用来说是很重要的,这时全局变量极容易导致代码脆弱。简而言之很不建议在全局变量中储存状态。

不要解构

只有对象才能拥有 MobX 的可观察性。解构时获得的基本类型变量都会保留其最新的值,但会失去可观察性。可以转而使用 boxed observable 来跟踪基本类型变量,也可以直接将其包裹在整个状态对象里。

// do NOT do this, it breaks reactivity
const {
  userCateStore: { activeUserCate },
} = useRootStore()
const localStore = useLocalStore(() => ({
  get dataSource() {
    return activeUserCate
  },
}))

这种情况下你可以将整个组件包裹在 observer 中或是仅分解到最近的上级父对象。

const { userCateStore } = useRootStore()
const localStore = useLocalStore(() => ({
  get dataSource() {
    return userCateStore.activeUserCate
  },
}))

可变状态

MobX 状态的一个重要特点在于其可变性。

Redux 和 useReducer 等其它一些流行解决方案相反,它们适合处理不可变的数据类型,而 MobX 则基于其可变性来将变更通知给所有订阅者。

由于对 MobX 状态对象的引用不随时间而发生变动,你可以避免使用不可变状态时所需的各种检查。

经常能见到 shouldComponentUpdate 中会放置体积庞大的检查以避免不必要的渲染,这在 MobX 中则是不必要的。

组件状态

since mobx-react-lite@1.3.0

useLocalStore<T, S>(initializer: () => T, source?: S): T

本地可观察状态可以使用 useLocalStore 的 hook 来引入,它会运行一次初始化函数来创建可观察储存,并在组件的一个生命周期中保留它。

返回对象的所有属性都会自动获得可观察性,其中的 getter 会被转换为计算属性,其它的方法会被绑定到储存上并自动应用 mobx transactions,如果初始化函数返回了一个新的类实例则会原样保留。

注意使用本地储存会与 concurrent 渲染等 React 特性发生冲突。

import React from 'react'
import { useLocalStore, useObserver } from 'mobx-react' // 6.x

export const SmartTodo = () => {
  const todo = useLocalStore(() => ({
    title: 'Click to toggle',
    done: false,
    toggle() {
      todo.done = !todo.done
    },
    get emoji() {
      return todo.done ? '' : ''
    },
  }))

  return useObserver(() => (
    <h3 onClick={todo.toggle}>
      {todo.title} {todo.emoji}
    </h3>
  ))
}

全局储存

useLocalStore 的名称表明这个储存是在组件中创建的,然而这不代表你不能在组件树中传递。实际上它完全可以用于全局管理状态而不必拘泥于名字,比如你可以将一组本地储存聚合成一个根对象并通过 React Context 将其传递给整个 app。

非可观察依赖

since mobx-react-lite@1.4.0 or mobx-react@6.0

注意对于每个组件实例每个储存只会被创建一次,它不能通过指定依赖来强制重渲染,你也不应当在初始化函数中直接引用任何不可观察的内容,因为其发生的变化无法被传播。

useLocalStore 的第二个参数允许传递包含非可观察对内容的普通对象,你可以在储存的引用派生里使用它。它可以是来自 prop,useContext 甚至 useReducer 等等任何你想混入的变量。传递给第二个参数的对象总是应该有相同的形态(无条件分支)。

import { observer, useLocalStore } from 'mobx-react' // 6.x
export const Counter = observer(props => {
  const store = useLocalStore(
    // don't ever destructure source, it won't work
    source => ({
      count: props.initialCount,
      get multiplied() {
        // you shouldn't ever refer to props directly here, it won't see a change
        return source.multiplier * store.count
      },
      inc() {
        store.count += 1
      },
    }),
    props, // note props passed here
  )
  return (
    <>
      <button id="inc" onClick={store.inc}>
        {`Count: ${store.count}`}
      </button>
      <span>{store.multiplied}</span>
    </>
  )
})

注意其内部使用的是 useAsObservableSource 的 hook 来包裹传入的对象。如果你无需使用 action 或计算属性也可以直接使用这个 hook。

状态外包

since mobe-react-lite@1.3.0

有时可能会需要对一组变量使用 MobX 的功能,同时还要保持原有逻辑。

useAsObservableSource<T>(state: T): T

useAsObservableSource 的 hook 可以将任何变量集合转换为可观察对象并保留一个稳定的引用(每次 hook 都会返回同一个对象)。

这个 hook 可以让包括 prop 和 state 等 React 原始类型在本地成为可观察对象,这样 储存初始化器 和 effect 就可以安全地引用它们,并且能获知其中任何值发生的变化,此外还可以将一组变量包装在可观察对象中来将他们传递给其它组件。

传给 useAsObservableSource 的参数只能是对象,只有表层属性会被转换。要进行深层转换请搭配使用 mobx.observableReact.useState

useAsObservableSource 返回的对象是可观测的,但出于一些原因实际编程时请将它看作是只读的。

提示:为了性能最优化,建议不要在同一个组件上同时使用 useAsObservableSource 和 useObserver(以及 observer),否则会触发重复渲染。可以转而使用 Observer 组件

import { useAsObservableSource } from 'mobx-react' // 6.x or mobx-react-lite@1.3.0
const PersonSource = ({ name, age }) => {
  const person = useAsObservableSource({ name, age })
  return <PersonBanner person={person} />
}

为什么要这样运作?

你可能会想:为什么不直接在渲染时进行计算?

对于上面这样简单的例子是没问题的,但是要知道实际的计算会复杂得多,这时就最好将它们统一放置。

否则要在组件树中向下传递储存时,每一处都会重复进行不必要的计算。

不要解构

这篇文章 专门讲解了关于解构的问题,由于它是常见的错误来源,所以这里还是要强调一下。请勿像这样 const { multiplier } = useAsObservableSource(props) 直接解构可观察对象!

为 React 组件赋予反应能力

为组件添加反应能力的方式有以下三种:

大部分情况下它们差别不大,这里你可以大概了解一下这三种不同的方式:

import { observable } from 'mobx'
import { Observer, useObserver, observer } from 'mobx-react' // 6.x or mobx-react-lite@1.4.0
import ReactDOM from 'react-dom'

const person = observable({
  name: 'John',
})

// named function is optional (for debugging purposes)
const P1 = observer(function P1({ person }) {
  return <h1>{person.name}</h1>
})

const P2 = ({ person }) => <Observer>{() => <h1>{person.name}</h1>}</Observer>

const P3 = ({ person }) => {
  return useObserver(() => <h1>{person.name}</h1>)
}

ReactDOM.render(
  <div>
    <P1 person={person} />
    <P2 person={person} />
    <P3 person={person} />
  </div>,
)

setTimeout(() => {
  person.name = 'Jane'
}, 1000)

为何需要可观察性?

MobX 是基于可观察性的概念开发的,初见时可能会觉得像魔术一样奇妙和无迹可寻,但它的原理其实很简单。

如果你曾接触过 发布-订阅模式(e.g. EventEmitter),可观察性其实是非常相似的概念,只是有一个易于使用的包装。

✨ 它会自动管理订阅,而你只需声明所需的数据。

⏱ 它在时间上是稳定的,在任何时候进行观察都能获取最新的数据。

这些特性在 React 中十分好用,你可以使用统一的组件编写方式并且很方便地处理数据,而无需额外的工作和重复的样板代码。

function LogoutWidget() {
  const { user } = useStore()
  return useObserver(() => (
    <Link to="/logout">
      <span className="name">{user.name}</span>
    </Link>
  ))
}

使用常规的 React 范式(e.g. useReducer)的组件无法通过改变 user.name 来重绘自己。而是需要父组件发起一个对子组件的更新,这肯定是非常低效的。

而通过 observer,组件自己就能感知到发生改变(只要 user 是可观察的),还能在父组件无感知的情况下进行重绘。

observer HOC

这是为组件添加反应能力的最优选择。请确保你已经按照 React 文档 中的介绍对 HOC 模式有了基本的认识。

observer<P>(baseComponent: React.FC<P>, options?: IObserverOptions): React.FC<P>

interface IObserverOptions {
  // Pass true to use React.forwardRef over the inner component. It's false by the default.
  forwardRef?: boolean
}

mobx-react 也可以观察类组件的变化,但它不支持以下提到的方式。

observer 可以将组件转换成反应性组件,它会自动追踪使用的可观察值并在它们发生改变时重绘组件。

observer 会将 React.memo 应用到组件上,因为被包裹的组件需要基于复杂的 prop 进行重绘。

⚠ 警告 如果包裹在 observer 内的组件依赖 [传统 context] 进行更新,底层的 React.memo 会阻断更新的传播。 尽量避免使用传统的 context 并使用 Observer 组件或 useObserver 的 hook。

import { observer, useLocalStore } from 'mobx-react' // 6.x or mobx-react-lite@1.4.0

export const Counter = observer<Props>(props => {
  const store = useLocalStore(() => ({
    count: props.initialCount,
    inc() {
      store.count += 1
    },
  }))

  return (
    <div>
      <span>{store.count}</span>
      <button onClick={store.inc}>Increment</button>
    </div>
  )
})

Observer 组件

<Observer>{renderFn}</Observer>

mobx-reactmobx-react-lite 的工作原理相同,对类组件也不例外。

它是一个 React 组件,能将 observer 应用到你编写的组件中的一个匿名区域。

它的子组件只能是单个的无参数函数,且这个函数必须返回一个 React 组件。

该函数中的渲染会被追踪,需要时会自动重绘。

当需要把渲染功能交给外部组件时(例如 React Native 的 listview),或是出于性能原因仅观察输出中相关的部分。

import { Observer, useLocalStore } from 'mobx-react' // 6.x or mobx-react-lite@1.4.0

export function ObservePerson() {
  const person = useLocalStore(() => ({ name: 'John' }))
  return (
    <div>
      {person.name} <i>I will never change my name</i>
      <div>
        <Observer>{() => <div>{person.name}</div>}</Observer>
        <button onClick={() => (person.name = 'Mike')}>
          I want to be Mike
        </button>
      </div>
    </div>
  )
}

注意嵌套

Observer 只能感知其渲染函数内部的可观察变量,如果你在内部使用其它的 render prop 组件将无法感知其变化。参考下面的例子:

import { Observer } from 'mobx-react' // 6.x or mobx-react-lite@1.4.0

function ObservePerson() {
  return (
    <Observer>
      {() => (
        <GetStore>{store => <div>{store.wontSeeChangesToThis}</div>}</GetStore>
      )}
    </Observer>
  )
}

你只能将其包裹在外层,或者尝试使用 React Hook。

import { Observer } from 'mobx-react' // 6.x or mobx-react-lite@1.4.0

function ObservePerson() {
  return (
    <GetStore>
      {store => (
        <Observer>{() => <div>{store.changesAreSeenAgain}</div>}</Observer>
      )}
    </GetStore>
  )
}

useObserver hook

useObserver<T>(fn: () => T, baseComponentName = "observed", options?: IUseObserverOptions): T

interface IUseObserverOptions {
  // optional custom hook that should make a component re-render (or not) upon changes
  useForceUpdate: () => () => void
}

这个 hook 仅在 mobx-react-lite 和 mobx-react@6 中可用。

它同时也是 observer HOC 和 Observer 组件内部的底层实现。

它允许你使用类似 observer 的行为,但允许你通过自己的方式来优化组件(例如使用带有自定义 areEqual 的 memo 或是使用 forwardRef 等等)并只声明特定的部分为可观测的。

如果有 hook 修改了可观察变量,组件也不会进行不必要的二次渲染。

import { useObserver, useLocalStore } from 'mobx-react' // 6.x or mobx-react-lite@1.4.0

function Person() {
  const person = useLocalStore(() => ({ name: 'John' }))
  return useObserver(() => (
    <div>
      {person.name}
      <button onClick={() => (person.name = 'Mike')}>No! I am Mike</button>
    </div>
  ))
}

在组件中使用 useObserver 的 hook 会导致可观察变量发生改变时重新渲染整个组件。如果你需要控制更细粒度的渲染,高级用户可以尝试 <Observer />useForceUpdate 两种方法。