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 传递到组件中(如上面的示例所示)
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
返回的更新器函数。
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 提供的额外功能包括
- 支持 React 类组件。
Provider
和inject
。MobX 自己实现的 React.createContext 前身,现在不再需要。- 可观察对象特定的
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 中不会显示任何显示名称。
可以使用以下方法来解决此问题
使用带有名称的
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 中修复。
现在您可以看到组件名称了
{🚀} 提示: 当将 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 优化 {🚀} 部分。
故障排除
救命!我的组件没有重新渲染...
- 确保您没有忘记
observer
(是的,这是最常见的错误)。 - 验证您打算响应的对象确实是可观察的。如果需要,请在运行时使用诸如
isObservable
、isObservableProp
之类的实用程序来验证这一点。 - 检查浏览器中的控制台日志以查找任何警告或错误。
- 确保您了解跟踪的一般工作原理。请查看 了解响应性 部分。
- 阅读上面描述的常见陷阱。
- 配置 MobX 以警告您不合理的机制用法,并检查控制台日志。
- 使用 trace 来验证您是否正在订阅正确的内容,或者使用 spy / mobx-log 包来检查 MobX 的一般行为。