React integration(在React中集成MobX)

React integration(在React中集成MobX)

用法:

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

const MyComponent = observer(props => ReactElement)

虽然MobX独立于React工作,但它们通常一起使用,在The gist of MobX中,你已经见到了集成中最重要的部分:可以封装React组件的observer 高阶组件

observer安装过程中所选择的单独的React bundle提供。在本例子中,我们将使用更轻量级的mobx-react-litepackage

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()

// 用`observer`包装的函数组件将对每一个引用的可观察对象的每一个变化做出反应。
const TimerView = observer(({ timer }) => <span>Seconds passed: {timer.secondsPassed}</span>)

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

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

提示:你可以在CodeSandbox尝试上面的例子。

observer高阶组件会自动订阅React组件渲染过程中使用的每一个可观察对象,当相关的可观察对象发生变化时,组件将自动重新渲染。同时,它还确保了组件在没有相关更改时不会重新渲染,因此,组件可访问但没被实际读取的可观察对象永远不会导致重新渲染。

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

对于observer而言,可观察对象如何到达组件并不重要,只要在组件中直接读取就好。像todos[0].authors.displayName这样深度读取可观察对象的复杂表达式也可以直接使用。与其他必须显式声明数据依赖关系,或预先计算数据依赖关系(例如选择器)的框架相比,MobX的订阅机制更加精确和高效。

内部状态和外部状态

state的组织方式有很大的灵活性,尽管(从技术上讲)我们读取到的是什么可观察对象,或者可观察对象来自哪里并不重要。外部和内部可观察状态在由observer包装的组件中使用的不同方式示例如下。

在observer组件中使用外部状态

  1. 通过props参数
// 被观察对象可以作为props传递到组件中

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

const myTimer = new Timer()  // 请参见上面的计时器定义

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

// 将myTimer作为props传递
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
  1. 通过全局变量
    如何获得可观察对象的引用并不重要,因此我们可以直接使用外部作用域中的可观察对象
const myTimer = new Timer()  // 请参见上面的计时器定义

// 没有props,`myTimer`直接从外部作用域获取
const TimerView = observer(() => <span>Seconds passed: {myTimer.secondsPassed}</span>)

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

直接使用可观察对象非常有效,但由于这通常会引入module state,因此此模式可能会使单元测试变得复杂。相反,我们建议改用React Context。

  1. 使用React Context
    React Context是与整个子树共享可观测对象的一种很好的机制:
import {observer} from 'mobx-react-lite'
import {createContext, useContext} from "react"

const TimerContext = createContext<Timer>()

const TimerView = observer(() => {
    // 在上下文中提取timer
    const timer = useContext(TimerContext)  // 请参见上面的计时器定义
    return (
        <span>Seconds passed: {timer.secondsPassed}</span>
    )
})

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

请注意,我们不建议手动更新Providervalue。或者说使用MobX时不需要这样做,因为共享的可观察对象可以自己更新。

在observer组件中使用内部状态

observer使用的可观察对象也可以是内部状态。同样,我们可以有几种不同的选择。

  1. 以可观察类的方式使用useState

使用本地可观察状态的最简单方法是使用useState存储对可观察类的引用。请注意,我们通常不想替换引用,因此完全可以忽略useState返回的更新函数。

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

const TimerView = observer(() => {
    const [timer] = useState(() => new Timer())  // 请参见上面的计时器定义
    return <span>Seconds passed: {timer.secondsPassed}</span>
})

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

如果想要像原始示例那样自动更新计时器,则可以使用useEffect:

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

2.以可观察对象的方式使用useState
如前所述,我们可以用 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)

3.使用useLocalObservable hook
const [store] = useState(() => observable({ /* something */}))这样的代码非常常见,使用mobx-react-lite包中的useLocalObservable hook可以将上面的示例简化为:

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

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

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

你可能不需要内部可观察状态

一般来说,我们建议不要轻易使用MobX observable来获取局部组件状态,因为从理论上讲,这可能会让你无法使用React Suspense机制的某些特性。最佳实践是,当状态捕获组件(包括子组件)之间共享域数据时,使用MobX observable。例如待办事项、用户、预订等。

只捕捉UI的状态,如加载状态、选择状态时,使用useState hook可能会更好,因为这将允许你在未来利用React Suspense的某些特性。

总是在observer组件内部读取可观察对象

你可能想知道,该什么时候使用observer。最佳实践是:把observer应用到所有你希望读取可观察对象的组件中

observer只增强被他装饰的组件,而不是它调用的组件。所以通常所有的组件都应该被observer包装。不要担心。。这并不会降低效率。相反,更多的、更精细的observer组件会使渲染变得更高效。

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

如果你想尽可能长时间地传递对象引用,并且只在基于observer的组件中读取它们的属性,并将它们渲染到DOM或子组件中,那么observer的工作效果最好。

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

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

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

请注意,这是一种不同于Reaction-Redux等其他库的思维方式,在Reaction-Redux库中,提前取消引用并向下传递原语是一种很好的做法,以更好地利用内存。如果不能完全理解,请查看Understanding reactivity部分
`

不要将observable传递给非observer的组件

observer包装的组件只订阅在它们自己渲染组件时使用到的可观察对象。因此,如果可观察对象/数组/映射被传递给子组件,它们也必须被包装为observer。这也适用于任何基于回调的组件。

如果你希望将observer对象传递给非observer的组件(因为它是第三方组件,或者因为您希望保持该组件的MobX不可知),则必须在传递之前 将可观察对象转换为普通的JavaScript值或结构

为了进一步说明上面的问题,来看看下面可观察的todo对象的例子:一个TodoView组件(观察者)和一个虚构的GridRow组件,它接受列 / 值映射,但未使用 observer

class Todo {
    title = "test"
    done = true

    constructor() {
        makeAutoObservable(this)
    }
}

const TodoView = observer(({ todo }: { todo: Todo }) =>
   // 错误:GridRow不会获取todo.title/todo.do中的更改,因为它不是观察者
   return <GridRow data={todo} />

   // 正确:让`TodoView`检测`todo`中的相关变化,并向下传递普通数据。
   return <GridRow data={{
       title: todo.title,
       done: todo.done
   }} />

   // 正确:使用`toJS`也可以,但显式通常更好。
   return <GridRow data={toJS(todo)} />
)

回调组件可能需要<Observer>

想象一下同样的例子,GridRow使用了一个onRender回调函数。因为onRender是GridRow的渲染周期的一部分,而不是由TodoView渲染。所以我们必须确保回调组件使用了observer组件。或者,我们可以使用 <Observer />创建一个匿名observer

const TodoView = observer(({ todo }: { todo: Todo }) => {
    // 错误:GridRow不会获取todo.title/todo.do中的更改,因为它不是观察者
    return <GridRow onRender={() => <td>{todo.title}</td>} />

    // 正确:将回调包装在观察器中,以便能够检测更改。
    return <GridRow onRender={() => <Observer>{() => <td>{todo.title}</td>}</Observer>} />
})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351