集成React(react-integration)
用法:
import { observer } from "mobx-react-lite" // Or "mobx-react".
const MyComponent = observer(props => ReactElement)
MobX 可以独立于 React 运行, 但是他们通常是结合在一起使用, 在 Mobx的宗旨(The gist of MobX) 一文中你会经常看见集成React最重要的一部分:用于包裹React Component的 observer
HOC方法。
observer
是你可以自主选择的,在安装时(during installation)独立提供的 React bindings 包。 在下面的例子中,我们将使用更加轻量的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()
//被`observer`包裹的函数式组件会被监听在它每一次调用前发生的任何变化
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 components 中任何 在渲染期间 被使用的 可被观察的对象 。
因此, 当任何可被观察的对象 变化 发生时候 组件会自动进行重新渲染(re-render)。
它还会确保组件在 没有变化 发生的时候不会进行重新渲染(re-render)。
但是, 更改组件的可观察对象的不可读属性, 也不会触发重新渲染(re-render)。
在实际项目中,这一特性使得MobX应用程序能够很好的进行开箱即用的优化,并且通常不需要任何额外的代码来防止过度渲染。
要想让observer
生效, 并不需要关心这些对象 如何传递到 组件的(它们只要能传递给组件即可 ·译者注), 只需要关心他们是否是可读的。
深层嵌套的可观察对象也没有问题, 复杂的表达式类似 todos[0].author.displayName
也是可以使用的。
与其他必须显式声明或预先计算数据依赖关系的框架(例如 selectors)相比,这种发生的订阅机制就显得更加精确和高效。
本地与外部状态(Local and external state)
在 Mobx 可以非常灵活的组织或管理(state), 从(技术角度讲)它不关心我们如何读取可观察对象,也不关心他们来自哪里。
下面的例子将通过不同的设计模式去使用被 observer
包裹的组件。
observer
组件中使用外部状态 (Using external state in observer
components)
可被观察对象可以通过组件的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>)
// 通过props传递myTimer.
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
虽然我们不关心是 如何 引用(reference)的可观察对象,但是我们可以使用 (consume) 外部作用域(outer scopes directly)的可观察对象 (类似通过 import这样的方法, 等等):
const myTimer = new Timer() // Timer 定义在上面.
// 没有props, `myTimer` 立刻变成了闭包。
const TimerView = observer(() => <span>Seconds passed: {myTimer.secondsPassed}</span>)
ReactDOM.render(<TimerView />, document.body)
直接使用可观察对象效果很好,但是这通常会是通过模块引入,这种写法可能会使单元测试变得复杂。 因此,我们建议使用React Context。
使用React Context共享整个可观察子树是一种很不错的选择:
import {observer} from 'mobx-react-lite'
import {createContext, useContext} from "react"
const TimerContext = createContext<Timer>()
const TimerView = observer(() => {
// 从context中获取timer.
const timer = useContext(TimerContext) // 可以在上面查看 Timer的定义。
return (
<span>Seconds passed: {timer.secondsPassed}</span>
)
})
ReactDOM.render(
<TimerContext.Provider value={new Timer()}>
<TimerView />
</TimerContext.Provider>,
document.body
)
需要注意的是我们并不推荐每一个不同的 值(value)
都通过不同的 Provider
来传递 . 在使用Mobx的过程中不需要这样做, 因为共享的可观察对象会更新他自己。
observer
组件中使用局部可观察对象(Using local observable state in observer
components)
在因为使用 observer
的可观察对象可以来自任何地方, 他们也可以使用local state(全局的state·译者注)。
再次声明,不同操作方式对于我们而言都是有价值的。
使用全局可观察对象的最简单的方式就是通过useState
去存储一个全局可观察对象的引用。
需要注意的是, 因为我们不需要替换全局可观察对象的引用,所以我们其实可以完全不声明useState
的更新方法:
import { observer } from "mobx-react-lite"
import { useState } from "react"
const TimerView = observer(() => {
const [timer] = useState(() => new Timer()) // Timer的定义在上面(正如上面所说的那样这里我们忽略了更新方法的定义·译者注)。
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
如果你想要类似我们官方的例子那样自动更新 timer ,
使用useEffect
可能是 React 中比较典型的写法:
useEffect(() => {
const handle = setInterval(() => {
timer.increaseTimer()
}, 1000)
return () => {
clearInterval(handle)
}
}, [timer])
如刚才说的那样, 直接创建一个可观察的对象,而不是使用classes(这里的类指的是全局定义的Mobx state,在它们是使用class声明的)。 我们可以参考 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 */}))
是非常通用的一套写法, 为了简化这个写法我们可以调用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)
你可能并不需要局部的可观察状态 (You might not need locally observable state)
通常来讲,我们推荐在编写全局公用组件的时候不要立刻使用Mobx的可观察能力, 因为从技术角度来讲他可能会使你无法使用一些React 的 Suspense 的方法特性。 总的来说,使用Mobx的可观察能力会捕获组件间的域状态(domain data)可能会 (包含子组件的)。像是todo item, users, bookings, 等等(就是说最好不要用mobx共享一些组件内的状态·译者注)。
状态类似获取UI state, 类似加载的 state, 选择的 state,等等, 最好还是使用 useState
hook, 这样可以让你使用高级的 React suspense特性。
使用Mobx的可观察能力作为 React components 的一种状态补充,比如出现以下情况: 1) 层级很深, 2) 拥有计算属性 3) 需要共享状态给其它 observer
components。
observer
组件中使用可观察能力(Always read observables inside observer
components)
始终在你可能会感到疑惑, 我应该什么时候使用 observer
? 大体上说: _ observer
应用于所有组件的可观察数据 _ 。
observer
是使用修饰模式增强你的组件, 而不是它调用你的组件. 所以通常所有的组件都可能用了 observer
,但是不要担心, 它不会导致性能损失。从另一个角度讲, 更多的 observer
组件可以使渲染更高效,因为它们更新数据的颗粒度更细。
小贴士: 尽可能晚地从对象中获取值
只要你传递引用,observer
就可以很好的工作。只要获取到内部的属性,基于 observer
的组件 就会渲染到 DOM / low-level components(DOM一般是浏览器环境,low-level components 一般是RN环境·译者注)。换句话说, observer
会根据实际情况响应你定义的对象中的值的'引用'。
下面的例子中, TimerView
组件不会响应未来的更新,因为.secondsPassed
不是在 observer
组件内部读取的而是在外部读取的,因此它不会被追踪到:
const TimerView = observer(({ secondsPassed }) => <span>Seconds passed: {secondsPassed}</span>)
React.render(<TimerView secondsPassed={myTimer.secondsPassed} />, document.body)
需要注意的一点是它不同于其它的观念模式库像是 react-redux
那样, redux 中强调尽可能早的获取和传递原始值以获得更好的副作用响应。
如果你还是没有理解, 可以先阅读 理解响应式(Understanding reactivity) 这篇文章。
observer
的组件中(Don't pass observables into components that aren't observer
)
不要将可观察对象传递到 不是通过observer
包裹的组件 只可以 订阅到在 他们自己 渲染的期间的可观察对象. 如果要将可观察对象 objects / arrays / maps 传递到子组件中, 他们必须被 observer
包裹。
通过callback回调的组件也是一样。
如果你非要传递可观察对象到未被observer
包裹的组件中, 要么是因为它是第三方组件,要么你需要组件对Mobx无感知,那你必须在传递前 转换可观察对象为显式 (convert the observables to plain JavaScript values or structures) 。
关于上述的详细描述,
可以看一下下面的使用 todo
对象的例子, 一个 TodoView
(observer)组件和一个虚构的接收一组对象映射入参的不是observer
的GridRow
组件:
class Todo {
title = "test"
done = true
constructor() {
makeAutoObservable(this)
}
}
const TodoView = observer(({ todo }: { todo: Todo }) =>
// 错误: GridRow 不能获取到 todo.title/ todo.done 的变更
// 因为他不是一个观察者(observer。
return <GridRow data={todo} />
// 正确:在 `TodoView` 中显式的声明相关的`todo` ,
// 到data中。
return <GridRow data={{
title: todo.title,
done: todo.done
}} />
// 正确: 使用 `toJS`也是可以的, 并且是更清晰直白的方式。
return <GridRow data={toJS(todo)} />
)
<Observer>
( Callback components might require <Observer>
)
回调组件可能会需要想象一下在同样的例子中, GridRow
携带一个 onRender
回调函数。
onRender
是 GridRow
渲染生命周期的一部分, 而不是 TodoView
的render (甚至在语法层面都能看出来),我们不得不保证回调组件是一个 observer
组件。
或者,我们可以使用 <Observer />
创建一个匿名观察者:
const TodoView = observer(({ todo }: { todo: Todo }) => {
// 错误: GridRow.onRender 不能获得 todo.title / todo.done 中的改变
// 因为它不是一个观察者(observer) 。
return <GridRow onRender={() => <td>{todo.title}</td>} />
// 正确: 将回调组件通过Observer包裹将会正确的获得变化。
return <GridRow onRender={() => <Observer>{() => <td>{todo.title}</td>}</Observer>} />
})
小贴士
注意: mobx-react vs. mobx-react-lite
在本文中我们使用 mobx-react-lite
作为默认包。
mobx-react 是他的大兄弟,它里面也引用了 mobx-react-lite
包。
它提供了很多在新项目中不再需要的特性, mobx-react附加的特性有:
- 对于React class components的支持。
Provider
和inject
. MobX的这些东西在有 React.createContext 替代后变得不必要了。- 特殊的观察对象
propTypes
。
要注意 mobx-react
是全量包,也会暴露 mobx-react-lite
包中的任何方法,其中包含对函数组件的支持。
如果你使用 mobx-react
,那就不要添加 mobx-react-lite
的依赖和引用了。
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中显示。
以下的手段可以修复这问题:
不要使用箭头函数而要使用带有命名的
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)
使用default export 导出, 会通过变量名称推断:
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 会出现这个 bug: https://github.com/facebook/react/issues/18026, 但是在 React 17 会被修复。
现在你应该可以看见组件名了:
当 observer
需要和装饰器或者其他高阶组件(HOC)一起使用时,请确保 observer
是最内层的 (最先调用的) 装饰器,否则的话它可能不会工作。
import { observer, useLocalObservable } from "mobx-react-lite"
import { useEffect } from "react"
const TimerView = observer(({ offset }) => {
const timer = useLocalObservable(() => ({
offset, // 初始化offset
secondsPassed: 0,
increaseTimer() {
this.secondsPassed++
},
get offsetTime() {
return this.secondsPassed - this.offset // 这里的'offset'不是'props'传入的那个
}
}))
useEffect(() => {
//同步来自 'props' 的偏差到可观察对象 'timer'
timer.offset = offset
}, [offset])
//作为demo用途,初始化一个定时器.
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
可以被用于触发需要发生的副作用, 它将会被约束在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方法之上触发可观察对象变化。
useEffect(
() =>
autorun(() => {
if (timer.secondsPassed > 60) alert("Still there. It's a minute already?!!")
}),
[]
)
// 作为demo用途在Effect里定义一个定时器。
useEffect(() => {
const handle = setInterval(timer.increaseTimer, 1000)
return () => {
clearInterval(handle)
}
}, [])
return <span>Seconds passed: {timer.secondsPassed}</span>
})
ReactDOM.render(<TimerView />, document.body)
需要注意的是我们在effect方法返回了一个创建自autorun
的清除方法。
这一点是非常重要的, 因为他确保了 autorun
在组件卸载的时候被清除了!
依赖数组可以保持为空,除非是一个不可观察对象的值需要触发autorun重新运行,你才需要将它添加到这里面。
请确保你的格式正确,你可以创建一个定时器(timer)
(上面的例子中) 作为依赖。
这是安全并且无副作用的, 因为它引用的依赖根本不会改变。
如果你不想显式的在Effect中定义可观察对象请使用reaction
而不是autorun
,他们的传参是完全相同的。
我如何能进一步的优化的我的React组件?(How can I further optimize my React components?)
请查看React的优化(React optimizations {🚀}) 这篇文章。
疑难解答
Help!我的组件没有进行重绘...
- 请确保你没有遗漏
observer
(是的,这是最常见的错误)。 - 请检查你传入的对象确定是可观察对象. 可以使用
isObservable
这个工具函数, 如果需要在运行时检查可以使用这个工具函数isObservableProp
。 - 请检查在浏览器控制台中的任何错误或者警告。
- 请确保你大体上是理解Mobx的调用栈. 详细请阅读 理解响应式(Understanding reactivity) 这篇文章。
- 请阅读上面小贴士中提及的常见错误。
- 配置(Configure) MobX 如何警告你的机制和输出日志。
- 使用 追踪(trace) 来确保你传递给Mobx了正确的东西,或者是否正确使用了Mobx的 spy / mobx-logger 包。