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-lite
package。
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组件中使用外部状态
- 通过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)
- 通过全局变量
如何获得可观察对象的引用并不重要,因此我们可以直接使用外部作用域中的可观察对象
const myTimer = new Timer() // 请参见上面的计时器定义
// 没有props,`myTimer`直接从外部作用域获取
const TimerView = observer(() => <span>Seconds passed: {myTimer.secondsPassed}</span>)
ReactDOM.render(<TimerView />, document.body)
直接使用可观察对象非常有效,但由于这通常会引入module state,因此此模式可能会使单元测试变得复杂。相反,我们建议改用React Context。
- 使用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
)
请注意,我们不建议手动更新Provider
的value
。或者说使用MobX时不需要这样做,因为共享的可观察对象可以自己更新。
在observer组件中使用内部状态
observer使用的可观察对象也可以是内部状态。同样,我们可以有几种不同的选择。
- 以可观察类的方式使用
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>} />
})