MobX 🇺🇦

MobX 🇺🇦

  • API 参考
  • 中文
  • 한국어
  • 赞助商
  • GitHub

›MobX 和 React

介绍

  • 关于 MobX
  • 关于此文档
  • 安装
  • MobX 的精髓

MobX 核心

  • 可观察状态
  • 动作
  • 计算值
  • 反应 {🚀}
  • API

MobX 和 React

  • React 集成
  • React 优化 {🚀}

提示与技巧

  • 定义数据存储
  • 理解反应性
  • 子类化
  • 分析反应性 {🚀}
  • 带参数的计算值 {🚀}
  • MobX-utils {🚀}
  • 自定义可观察值 {🚀}
  • 延迟可观察值 {🚀}
  • 集合实用程序 {🚀}
  • 拦截和观察 {🚀}

微调

  • 配置 {🚀}
  • 装饰器 {🚀}
  • 从 MobX 4/5 迁移 {🚀}
编辑

React 集成

用法

import { observer } from "mobx-react-lite" // Or "mobx-react".

const MyComponent = observer(props => ReactElement)

虽然 MobX 独立于 React 工作,但它们通常一起使用。在 MobX 的精髓 中,您已经看到了此集成中最重要的部分:observer HoC,您可以将其包装在 React 组件周围。

observer 由一个单独的 React 绑定包提供,您可以在 安装期间 选择它。在本例中,我们将使用更轻量级的 mobx-react-lite 包。

import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react-lite"

class Timer {
    secondsPassed = 0

    constructor() {
        makeAutoObservable(this)
    }

    increaseTimer() {
        this.secondsPassed += 1
    }
}

const myTimer = new Timer()

// A function component wrapped with `observer` will react
// to any future change in an observable it used before.
const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)

ReactDOM.render(<TimerView timer={myTimer} />, document.body)

setInterval(() => {
    myTimer.increaseTimer()
}, 1000)

提示:您可以在 CodeSandbox 上自己试用上面的示例。

observer HoC 会自动将 React 组件订阅到渲染期间使用的所有可观察值。因此,组件将在相关可观察值发生变化时自动重新渲染。它还确保组件在没有相关变化时不会重新渲染。因此,可访问组件但实际上没有读取的可观察值永远不会导致重新渲染。

在实践中,这使 MobX 应用程序开箱即用地得到了很好的优化,并且通常不需要任何额外的代码来防止过度渲染。

对于 observer 的工作,如何获取可观察值无关紧要,重要的是它们被读取了。深度读取可观察值是可以的,像 todos[0].author.displayName 这样的复杂表达式开箱即用。与其他必须显式声明数据依赖关系或进行预计算(例如选择器)的框架相比,这使得订阅机制更加精确和高效。

局部和外部状态

状态的组织方式非常灵活,因为我们读取哪些可观察值以及可观察值来自何处并不重要(从技术上讲)。以下示例演示了如何在使用 observer 包装的组件中使用外部和局部可观察状态的不同模式。

在 observer 组件中使用外部状态

使用 props
使用全局变量
使用 React 上下文

可观察值可以作为 props 传递到组件中(如上面的示例所示)

import { observer } from "mobx-react-lite"

const myTimer = new Timer() // See the Timer definition above.

const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)

// Pass myTimer as a prop.
ReactDOM.render(<TimerView timer={myTimer} />, document.body)

由于如何获取可观察值的引用并不重要,因此我们可以直接从外部范围(包括从导入等)中使用可观察值。

const myTimer = new Timer() // See the Timer definition above.

// No props, `myTimer` is directly consumed from the closure.
const TimerView = observer(() => <span>Seconds passed: {myTimer.secondsPassed}</span>)

ReactDOM.render(<TimerView />, document.body)

直接使用可观察值效果很好,但由于这通常会引入模块状态,这种模式可能会使单元测试变得复杂。相反,我们建议使用 React 上下文。

React 上下文 是一种将可观察值与整个子树共享的绝佳机制。

import {observer} from 'mobx-react-lite'
import {createContext, useContext} from "react"

const TimerContext = createContext<Timer>()

const TimerView = observer(() => {
// Grab the timer from the context.
const timer = useContext(TimerContext) // See the Timer definition above.
return (
<span>Seconds passed: {timer.secondsPassed}</span>
)
})

ReactDOM.render(
<TimerContext.Provider value={new Timer()}>
<TimerView />
</TimerContext.Provider>
,
document.body
)

请注意,我们不建议将 Provider 的 value 替换为另一个。使用 MobX,应该没有必要这样做,因为共享的可观察值可以自行更新。

在 observer 组件中使用局部可观察状态

由于 observer 使用的可观察值可以来自任何地方,因此它们也可以是局部状态。同样,我们有不同的选择。

`useState` 与可观察类
`useState` 与局部可观察对象
`useLocalObservable` 钩子

使用局部可观察状态的最简单方法是使用 useState 存储对可观察类的引用。请注意,由于我们通常不希望替换引用,因此我们完全忽略了 useState 返回的更新器函数。

import { observer } from "mobx-react-lite"
import { useState } from "react"

const TimerView = observer(() => {
const [timer] = useState(() => new Timer()) // See the Timer definition above.
return <span>Seconds passed: {timer.secondsPassed}</span>
})

ReactDOM.render(<TimerView />, document.body)

如果要像在原始示例中那样自动更新计时器,可以使用典型的 React 方式在 useEffect 中使用它。

useEffect(() => {
const handle = setInterval(() => {
timer.increaseTimer()
}, 1000)
return () => {
clearInterval(handle)
}
}, [timer])

如前所述,除了使用类之外,还可以直接创建可观察对象。我们可以利用 observable 来实现这一点。

import { observer } from "mobx-react-lite"
import { observable } from "mobx"
import { useState } from "react"

const TimerView = observer(() => {
const [timer] = useState(() =>
observable({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
})
)
return <span>Seconds passed: {timer.secondsPassed}</span>
})

ReactDOM.render(<TimerView />, document.body)

组合 const [store] = useState(() => observable({ /* something */})) 非常常见。为了简化这种模式,useLocalObservable 钩子从 mobx-react-lite 包中暴露出来,使之前的示例可以简化为

import { observer, useLocalObservable } from "mobx-react-lite"

const TimerView = observer(() => {
const timer = useLocalObservable(() => ({
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
}
}))
return <span>Seconds passed: {timer.secondsPassed}</span>
})

ReactDOM.render(<TimerView />, document.body)

您可能不需要局部可观察状态

一般来说,我们建议不要太快地将 MobX 可观察值用于局部组件状态,因为这可能会从理论上讲阻止您使用 React Suspense 机制的一些功能。作为经验法则,当状态捕获在组件之间(包括子组件)共享的域数据时,请使用 MobX 可观察值。例如,待办事项、用户、预订等。

仅捕获 UI 状态的状态(如加载状态、选择等)可能更适合使用 useState 钩子,因为这将使您能够在未来利用 React Suspense 功能。

当可观察值在 React 组件内部使用时,一旦它们 1) 很深,2) 具有计算值或 3) 与其他 observer 组件共享,它们就会增加价值。

始终在 observer 组件内部读取可观察值

您可能想知道,何时使用 observer?经验法则:将 observer 应用于读取可观察数据的所有组件。

observer 仅增强您正在装饰的组件,而不是它调用的组件。因此,通常所有组件都应由 observer 包装。别担心,这不是低效的。相反,更多的 observer 组件会使渲染更加高效,因为更新变得更加细粒度。

提示:尽可能晚地从对象中获取值

如果您尽可能长时间地传递对象引用,并且仅在要将其渲染到 DOM/低级组件的 observer 基于组件内部读取它们的属性,则 observer 工作得最好。换句话说,observer 对您从对象中“取消引用”值这一事实做出反应。

在上面的示例中,如果 TimerView 组件被定义如下,它将不会对未来的变化做出反应,因为 .secondsPassed 不是在 observer 组件内部读取的,而是在外部读取的,因此没有被跟踪。

const TimerView = observer(({ secondsPassed }) => <span>Seconds passed: {secondsPassed}</span>)

React.render(<TimerView secondsPassed={myTimer.secondsPassed} />, document.body)

请注意,这与 react-redux 等其他库的思维方式不同,在 react-redux 中,尽早取消引用并将基本类型传递下去是一个很好的做法,以便更好地利用记忆化。如果问题还不完全清楚,请确保查看 理解反应性 部分。

不要将可观察值传递给不是 observer 的组件

使用 observer 包裹的组件 仅 订阅在其 自身 渲染组件期间使用的可观察对象。因此,如果可观察对象 / 数组 / 地图传递给子组件,则这些子组件也必须使用 observer 包裹。对于任何基于回调的组件,情况也是如此。

如果要将可观察对象传递给不是 observer 的组件,原因可能是它是第三方组件,或者因为希望保持该组件与 MobX 无关,则需要在传递之前 将可观察对象转换为普通 JavaScript 值或结构。

为了详细说明上述内容,请考虑以下示例可观察 todo 对象、TodoView 组件(observer)和一个假想的 GridRow 组件,它接受列 / 值映射,但不是 observer

class Todo {
    title = "test"
    done = true

    constructor() {
        makeAutoObservable(this)
    }
}

const TodoView = observer(({ todo }: { todo: Todo }) =>
   // WRONG: GridRow won't pick up changes in todo.title / todo.done
   //        since it isn't an observer.
   return <GridRow data={todo} />

   // CORRECT: let `TodoView` detect relevant changes in `todo`,
   //          and pass plain data down.
   return <GridRow data={{
       title: todo.title,
       done: todo.done
   }} />

   // CORRECT: using `toJS` works as well, but being explicit is typically better.
   return <GridRow data={toJS(todo)} />
)

回调组件可能需要 <Observer>

想象一下相同的示例,其中 GridRow 接受 onRender 回调。由于 onRender 是 GridRow 渲染循环的一部分,而不是 TodoView 的渲染(即使它在语法上出现在那里),我们必须确保回调组件使用 observer 组件。或者,我们可以使用 <Observer /> 创建一个内联匿名观察者

const TodoView = observer(({ todo }: { todo: Todo }) => {
    // WRONG: GridRow.onRender won't pick up changes in todo.title / todo.done
    //        since it isn't an observer.
    return <GridRow onRender={() => <td>{todo.title}</td>} />

    // CORRECT: wrap the callback rendering in Observer to be able to detect changes.
    return <GridRow onRender={() => <Observer>{() => <td>{todo.title}</td>}</Observer>} />
})

提示

服务器端渲染 (SSR)如果在服务器端渲染上下文中使用 observer;请确保调用 enableStaticRendering(true),以便 observer 不会订阅任何使用的可观察对象,并且不会引入任何 GC 问题。

注意:mobx-react vs. mobx-react-lite在本说明文档中,我们使用 mobx-react-lite 作为默认值。 mobx-react 是它的“大哥”,在内部使用 mobx-react-lite。它提供了一些在全新项目中通常不再需要的功能。mobx-react 提供的额外功能包括

  1. 支持 React 类组件。
  2. Provider 和 inject。MobX 自己实现的 React.createContext 前身,现在不再需要。
  3. 可观察对象特定的 propTypes。

请注意,mobx-react 完整地重新打包和重新导出 mobx-react-lite,包括函数组件支持。如果您使用 mobx-react,则无需将 mobx-react-lite 添加为依赖项,也无需从任何位置导入它。

注意: observer 或 React.memo? observer 会自动应用 memo,因此 observer 组件永远不需要被 memo 包裹。memo 可以安全地应用于观察者组件,因为如果相关,observer 会自动拾取道具内部的(深度)变异。

提示: 基于类的 React 组件的 observer如上所述,基于类的组件仅通过 mobx-react 支持,而不支持 mobx-react-lite。简而言之,您可以像包裹函数组件一样将基于类的组件包裹在 observer 中

import React from "React"

const TimerView = observer(
    class TimerView extends React.Component {
        render() {
            const { timer } = this.props
            return <span>Seconds passed: {timer.secondsPassed} </span>
        }
    }
)

请查看 mobx-react 文档 了解更多信息。

提示: 在 React DevTools 中显示漂亮的组件名称 React DevTools 使用组件的显示名称信息来正确显示组件层次结构。

如果您使用

export const MyComponent = observer(props => <div>hi</div>)

则在 DevTools 中不会显示任何显示名称。

devtools-noname

可以使用以下方法来解决此问题

  • 使用带有名称的 function 而不是箭头函数。mobx-react 从函数名推断组件名

    export const MyComponent = observer(function MyComponent(props) {
        return <div>hi</div>
    })
    
  • 转译器(如 Babel 或 TypeScript)从变量名推断组件名

    const _MyComponent = props => <div>hi</div>
    export const MyComponent = observer(_MyComponent)
    
  • 使用默认导出再次从变量名推断

    const MyComponent = props => <div>hi</div>
    export default observer(MyComponent)
    
  • [已损坏] 显式设置 displayName

    export const MyComponent = observer(props => <div>hi</div>)
    MyComponent.displayName = "MyComponent"
    

    这在撰写本文时在 React 16 中已损坏;mobx-react observer 使用 React.memo 并遇到了此错误:https://github.com/facebook/react/issues/18026,但将在 React 17 中修复。

现在您可以看到组件名称了

devtools-withname

{🚀} 提示: 当将 observer 与其他高阶组件组合使用时,请先应用 observer

当 observer 需要与其他装饰器或高阶组件组合使用时,请确保 observer 是最里面的(第一个应用的)装饰器;否则它可能完全不起作用。

{🚀} 提示: 从道具推断计算值在某些情况下,本地可观察对象的计算值可能取决于组件接收的一些道具。但是,React 组件接收的道具集本身不可观察,因此道具的更改不会反映在任何计算值中。您必须手动更新本地可观察对象状态才能正确地从最新数据中推断计算值。

import { observer, useLocalObservable } from "mobx-react-lite"
import { useEffect } from "react"

const TimerView = observer(({ offset = 0 }) => {
    const timer = useLocalObservable(() => ({
        offset, // The initial offset value
        secondsPassed: 0,
        increaseTimer() {
            this.secondsPassed++
        },
        get offsetTime() {
            return this.secondsPassed - this.offset // Not 'offset' from 'props'!
        }
    }))

    useEffect(() => {
        // Sync the offset from 'props' into the observable 'timer'
        timer.offset = offset
    }, [offset])

    // Effect to set up a timer, only for demo purposes.
    useEffect(() => {
        const handle = setInterval(timer.increaseTimer, 1000)
        return () => {
            clearInterval(handle)
        }
    }, [])

    return <span>Seconds passed: {timer.offsetTime}</span>
})

ReactDOM.render(<TimerView />, document.body)

实际上,您很少需要这种模式,因为 return <span>Seconds passed: {timer.secondsPassed - offset}</span> 是一个更简单,但效率稍低的解决方案。

{🚀} 提示: useEffect 和可观察对象

useEffect 可用于设置需要发生的副作用,这些副作用与 React 组件的生命周期绑定。使用 useEffect 需要指定依赖项。有了 MobX,这实际上并不需要,因为 MobX 已经有一种方法可以自动确定副作用的依赖项,即 autorun。幸运的是,将 autorun 与使用 useEffect 耦合到组件的生命周期非常简单

import { observer, useLocalObservable, useAsObservableSource } from "mobx-react-lite"
import { useState } from "react"

const TimerView = observer(() => {
    const timer = useLocalObservable(() => ({
        secondsPassed: 0,
        increaseTimer() {
            this.secondsPassed++
        }
    }))

    // Effect that triggers upon observable changes.
    useEffect(
        () =>
            autorun(() => {
                if (timer.secondsPassed > 60) alert("Still there. It's a minute already?!!")
            }),
        []
    )

    // Effect to set up a timer, only for demo purposes.
    useEffect(() => {
        const handle = setInterval(timer.increaseTimer, 1000)
        return () => {
            clearInterval(handle)
        }
    }, [])

    return <span>Seconds passed: {timer.secondsPassed}</span>
})

ReactDOM.render(<TimerView />, document.body)

请注意,我们从副作用函数中返回由 autorun 创建的取消器。这很重要,因为它可以确保 autorun 在组件卸载后被清理!

依赖项数组通常可以保留为空,除非非可观察对象的值应该触发 autorun 的重新运行,在这种情况下,您需要将其添加到那里。为了使您的 linter 满意,您可以将 timer(在上面的示例中)定义为依赖项。这很安全,没有进一步的影响,因为引用实际上永远不会改变。

如果您想明确定义哪些可观察对象应该触发副作用,请使用 reaction 而不是 autorun,除此之外,模式保持不变。

如何进一步优化我的 React 组件?

请查看 React 优化 {🚀} 部分。

故障排除

救命!我的组件没有重新渲染...

  1. 确保您没有忘记 observer(是的,这是最常见的错误)。
  2. 验证您打算响应的对象确实是可观察的。如果需要,请在运行时使用诸如 isObservable、isObservableProp 之类的实用程序来验证这一点。
  3. 检查浏览器中的控制台日志以查找任何警告或错误。
  4. 确保您了解跟踪的一般工作原理。请查看 了解响应性 部分。
  5. 阅读上面描述的常见陷阱。
  6. 配置 MobX 以警告您不合理的机制用法,并检查控制台日志。
  7. 使用 trace 来验证您是否正在订阅正确的内容,或者使用 spy / mobx-log 包来检查 MobX 的一般行为。
← APIReact 优化 {🚀} →
  • 本地和外部状态
    • 在 observer 组件中使用外部状态
    • 在 observer 组件中使用本地可观察对象状态
    • 您可能不需要本地可观察对象状态
  • 始终在 observer 组件内读取可观察对象
    • 提示:尽可能晚地从对象中获取值
    • 不要将可观察对象传递给不是 observer 的组件
    • 回调组件可能需要 <Observer>
  • 提示
    • 如何进一步优化我的 React 组件?
  • 故障排除
MobX 🇺🇦
文档
关于 MobXMobX 的要点
社区
GitHub 讨论 (NEW)Stack Overflow
更多
Star