React 集成
¥React integration
用法:
¥Usage:
import { observer } from "mobx-react-lite" // Or "mobx-react".
const MyComponent = observer(props => ReactElement)
虽然 MobX 独立于 React 工作,但它们最常一起使用。在 MobX 的要点 中你已经看到了这种集成最重要的部分:可以封装 React 组件的 observer
HoC。
¥While MobX works independently from React, they are most commonly used together. In The gist of MobX you have already seen the most important part of this integration: the observer
HoC that you can wrap around a React component.
observer
由你选择 安装期间 的单独 React 绑定包提供。在此示例中,我们将使用更轻量级的 mobx-react-lite
包。
¥observer
is provided by a separate React bindings package you choose during installation. In this example, we're going to use the more lightweight 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()
// 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 上自己玩一下上面的例子。
¥Hint: you can play with the above example yourself on CodeSandbox.
observer
HoC 自动将 React 组件订阅到渲染期间使用的任何可观察对象。因此,当相关可观察值发生变化时,组件将自动重新渲染。它还确保在没有相关更改时组件不会重新渲染。因此,组件可访问但未实际读取的可观察量永远不会导致重新渲染。
¥The observer
HoC automatically subscribes React components to any observables that are used during rendering.
As a result, components will automatically re-render when relevant observables change.
It also makes sure that components don't re-render when there are no relevant changes.
So, observables that are accessible by the component, but not actually read, won't ever cause a re-render.
实际上,这使得 MobX 应用开箱即用地得到了很好的优化,并且它们通常不需要任何额外的代码来防止过度渲染。
¥In practice this makes MobX applications very well optimized out of the box and they typically don't need any additional code to prevent excessive rendering.
为了让 observer
工作,可观察量如何到达组件并不重要,重要的是它们被读取。深入读取可观察量是一种很好的、复杂的表达式,就像 todos[0].author.displayName
开箱即用一样。与必须显式声明或预先计算数据依赖的其他框架(例如选择器)相比,这使得订阅机制更加精确和高效。
¥For observer
to work, it doesn't matter how the observables arrive in the component, only that they are read.
Reading observables deeply is fine, complex expression like todos[0].author.displayName
work out of the box.
This makes the subscription mechanism much more precise and efficient compared to other frameworks in which data dependencies have to be declared explicitly or be pre-computed (e.g. selectors).
本地和外部状态
¥Local and external state
状态的组织方式具有很大的灵活性,因为(从技术上讲)我们读取哪些可观察量或可观察量源自何处并不重要。下面的示例演示了如何在用 observer
封装的组件中使用外部和本地可观察状态的不同模式。
¥There is great flexibility in how state is organized, since it doesn't matter (technically that is) which observables we read or where observables originated from.
The examples below demonstrate different patterns on how external and local observable state can be used in components wrapped with observer
.
observer
组件中使用外部状态
在 ¥Using external state in observer
components
Observables 可以作为 props 传递到组件中(如上面的示例):
¥Observables can be passed into components as props (as in the example above):
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)
由于我们如何获取对可观察量的引用并不重要,因此我们可以直接从外部作用域使用可观察量(包括来自导入等):
¥Since it doesn't matter how we got the reference to an observable, we can consume observables from outer scopes directly (including from imports, etc.):
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 Context。
¥Using observables directly works very well, but since this typically introduces module state, this pattern might complicate unit testing. Instead, we recommend using React Context instead.
React 上下文 是一种与整个子树共享可观察量的出色机制:
¥React Context is a great mechanism to share observables with an entire subtree:
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,应该不需要这样做,因为共享的 observable 可以自行更新。
¥Note that we don't recommend ever replacing the value
of a Provider
with a different one. Using MobX, there should be no need for that, since the observable that is shared can be updated itself.
observer
组件中使用本地可观察状态
在 ¥Using local observable state in observer
components
由于 observer
使用的可观察量可以来自任何地方,因此它们也可以是本地状态。同样,我们有不同的选择。
¥Since observables used by observer
can come from anywhere, they can be local state as well.
Again, different options are available for us.
使用本地可观察状态的最简单方法是使用 useState
存储对可观察类的引用。请注意,由于我们通常不想替换引用,因此我们完全忽略 useState
返回的更新程序函数:
¥The simplest way to use local observable state is to store a reference to an observable class with useState
.
Note that, since we typically don't want to replace the reference, we totally ignore the updater function returned by 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
:
¥If you want to automatically update the timer like we did in the original example,
useEffect
could be used in typical React fashion:
useEffect(() => {
const handle = setInterval(() => {
timer.increaseTimer()
}, 1000)
return () => {
clearInterval(handle)
}
}, [timer])
如前所述,可以直接创建可观察对象,而不是使用类。我们可以利用 observable 来实现:
¥As stated before, instead of using classes, it is possible to directly create observable objects. We can leverage observable for that:
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
包中公开,从而可以将前面的示例简化为:
¥The combination const [store] = useState(() => observable({ /* something */}))
is
quite common. To make this pattern simpler the useLocalObservable
hook is exposed from mobx-react-lite
package, making it possible to simplify the earlier example to:
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)
你可能不需要本地可观察的状态
¥You might not need locally observable state
一般来说,我们建议不要过快地使用 MobX observables 来获取本地组件状态,因为理论上这会让你无法使用 React 的 Suspense 机制的某些功能。根据经验,当状态捕获组件(包括子组件)之间共享的域数据时,请使用 MobX 可观察量。例如待办事项、用户、预订等。
¥In general, we recommend to not resort to MobX observables for local component state too quickly, as this can theoretically lock you out of some features of React's Suspense mechanism. As a rule of thumb, use MobX observables when the state captures domain data that is shared among components (including children). Such as todo items, users, bookings, etc.
仅捕获 UI 状态(如加载状态、选择等)的状态可能更适合 useState
钩子,因为这将允许你在将来利用 React Suspense 功能。
¥State that only captures UI state, like loading state, selections, etc, might be better served by the useState
hook, since this will allow you to leverage React suspense features in the future.
在 React 组件中使用可观察量,只要它们 1) 深,2) 有计算值或 3) 与其他 observer
组件共享,就会增加值。
¥Using observables inside React components adds value as soon as they are either 1) deep, 2) have computed values or 3) are shared with other observer
components.
observer
组件内的可观察值
始终读取 ¥Always read observables inside observer
components
你可能想知道,我什么时候应用 observer
?经验法则是:将 observer
应用于读取可观察数据的所有组件。
¥You might be wondering, when do I apply observer
? The rule of thumb is: apply observer
to all components that read observable data.
observer
只增强你正在装饰的组件,而不是它调用的组件。所以通常所有的组件都应该用 observer
封装。别担心,这并不是低效的。相反,随着更新变得更加细粒度,更多的 observer
组件使渲染更加高效。
¥observer
only enhances the component you are decorating, not the components called by it. So usually all your components should be wrapped by observer
. Don't worry, this is not inefficient. On the contrary, more observer
components make rendering more efficient as updates become more fine-grained.
提示:尽可能晚地从对象中获取值
¥Tip: Grab values from objects as late as possible
如果你尽可能长时间地传递对象引用,并且仅在基于 observer
的组件中读取它们的属性(这些组件将把它们渲染到 DOM/底层组件中),那么 observer
效果最好。换句话说,observer
对你从对象中 'dereference' 一个值这一事实做出反应。
¥observer
works best if you pass object references around as long as possible, and only read their properties inside the observer
based components that are going to render them into the DOM / low-level components.
In other words, observer
reacts to the fact that you 'dereference' a value from an object.
在上面的示例中,如果 TimerView
组件定义如下,则它不会对未来的更改做出反应,因为 .secondsPassed
不是在 observer
组件内部读取,而是在外部读取,因此不会被跟踪:
¥In the above example, the TimerView
component would not react to future changes if it was defined
as follows, because the .secondsPassed
is not read inside the observer
component, but outside, and is hence not tracked:
const TimerView = observer(({ secondsPassed }) => <span>Seconds passed: {secondsPassed}</span>)
React.render(<TimerView secondsPassed={myTimer.secondsPassed} />, document.body)
请注意,这与 react-redux
等其他库的思维方式不同,在 react-redux
中,尽早取消引用并向下传递原语是一种很好的做法,以更好地利用记忆。如果问题不完全清楚,请务必查看 了解反应性 部分。
¥Note that this is a different mindset from other libraries like react-redux
, where it is a good practice to dereference early and pass primitives down, to better leverage memoization.
If the problem is not entirely clear, make sure to check out the Understanding reactivity section.
observer
的组件中
不要将可观察量传递到不是 ¥Don't pass observables into components that aren't observer
用 observer
封装的组件仅订阅在其自己渲染组件期间使用的可观察量。因此,如果可观察对象/数组/映射传递给子组件,则它们也必须用 observer
封装。对于任何基于回调的组件也是如此。
¥Components wrapped with observer
only subscribe to observables used during their own rendering of the component. So if observable objects / arrays / maps are passed to child components, those have to be wrapped with observer
as well.
This is also true for any callback based components.
如果你想将可观察量传递给不是 observer
的组件,无论是因为它是第三方组件,还是因为你想让该组件与 MobX 保持无关,你必须在传递它们之前先进行 将可观察量转换为纯 JavaScript 值或结构 操作。
¥If you want to pass observables to a component that isn't an observer
, either because it is a third-party component, or because you want to keep that component MobX agnostic, you will have to convert the observables to plain JavaScript values or structures before passing them on.
为了详细说明上述内容,请使用以下示例:可观察的 todo
对象、TodoView
组件(观察者)和一个虚构的 GridRow
组件,该组件采用列/值映射,但不是 observer
:
¥To elaborate on the above,
take the following example observable todo
object, a TodoView
component (observer) and an imaginary GridRow
component that takes a column / value mapping, but which isn't an 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>
回调组件可能需要 ¥Callback components might require <Observer>
想象一下同样的例子,其中 GridRow
接受 onRender
回调。由于 onRender
是 GridRow
渲染周期的一部分,而不是 TodoView
渲染的一部分(即使它在语法上出现的地方),我们必须确保回调组件使用 observer
组件。或者,我们可以使用 <Observer />
创建一个内联匿名观察者:
¥Imagine the same example, where GridRow
takes an onRender
callback instead.
Since onRender
is part of the rendering cycle of GridRow
, rather than TodoView
's render (even though that is where it syntactically appears), we have to make sure that the callback component uses an observer
component.
Or, we can create an in-line anonymous observer using <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>} />
})
提示
¥Tips
Server Side Rendering (SSR)
Ifobserver
is used in server side rendering context; make sure to call enableStaticRendering(true)
, so that observer
won't subscribe to any observables used, and no GC problems are introduced.
Note: mobx-react vs. mobx-react-lite
In this documentation we usedmobx-react-lite
as default.
mobx-react is it's big brother, which uses mobx-react-lite
under the hood.
It offers a few more features which are typically not needed anymore in greenfield projects. The additional things offered by mobx-react:
支持 React 类组件。
¥Support for React class components.
Provider
和inject
。MobX 自己的 React.createContext 前身不再需要了。¥
Provider
andinject
. MobX's own React.createContext predecessor which is not needed anymore.可观察到的具体
propTypes
。¥Observable specific
propTypes
.
请注意,mobx-react
完全重新打包并重新导出 mobx-react-lite
,包括功能组件支持。如果你使用 mobx-react
,则无需将 mobx-react-lite
添加为依赖或从任何地方导入。
¥Note that mobx-react
fully repackages and re-exports mobx-react-lite
, including functional component support.
If you use mobx-react
, there is no need to add mobx-react-lite
as a dependency or import from it anywhere.
Note: observer
or React.memo
?
observer
automatically applies memo
, so observer
components never need to be wrapped in memo
.
memo
can be applied safely to observer components because mutations (deeply) inside the props will be picked up by observer
anyway if relevant.
Tip: observer
for class based React components
As stated above, class based components are only supported through mobx-react
, and not mobx-react-lite
.
Briefly, you can wrap class-based components in observer
just like
you can wrap function components:
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 文档 了解更多信息。
¥Check out mobx-react docs for more information.
Tip: nice component names in React DevTools
React DevTools uses the display name information of components to properly display the component hierarchy.如果你使用:
¥If you use:
export const MyComponent = observer(props => <div>hi</div>)
那么 DevTools 中将看不到显示名称。
¥then no display name will be visible in the DevTools.
可以使用以下方法来解决此问题:
¥The following approaches can be used to fix this:
使用
function
与名称而不是箭头函数。mobx-react
从函数名称推断组件名称:¥use
function
with a name instead of an arrow function.mobx-react
infers component name from the function name:export const MyComponent = observer(function MyComponent(props) { return <div>hi</div> })
转译器(如 Babel 或 TypeScript)从变量名称推断组件名称:
¥Transpilers (like Babel or TypeScript) infer component name from the variable name:
const _MyComponent = props => <div>hi</div> export const MyComponent = observer(_MyComponent)
使用默认导出再次从变量名称推断:
¥Infer from the variable name again, using default export:
const MyComponent = props => <div>hi</div> export default observer(MyComponent)
[损坏] 显式设置
displayName
:¥[Broken] Set
displayName
explicitly: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 中修复。¥This is broken in React 16 at the time of writing; mobx-react
observer
uses a React.memo and runs into this bug: https://github.com/facebook/react/issues/18026, but it will be fixed in React 17.
现在你可以看到组件名称:
¥Now you can see component names:
{🚀} Tip: when combining observer
with other higher-order-components, apply observer
first
当 observer
需要与其他装饰器或高阶组件组合时,请确保 observer
是最里面(最先应用的)装饰器;否则它可能什么也不做。
¥When observer
needs to be combined with other decorators or higher-order-components, make sure that observer
is the innermost (first applied) decorator;
otherwise it might do nothing at all.
{🚀} Tip: deriving computeds from props
In some cases the computed values of your local observables might depend on some of the props your component receives. However, the set of props that a React component receives is in itself not observable, so changes to the props won't be reflected in any computed values. You have to manually update local observable state in order to properly derive computed values from latest data.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>
是一个更简单的解决方案,尽管效率稍低。
¥In practice you will rarely need this pattern, since
return <span>Seconds passed: {timer.secondsPassed - offset}</span>
is a much simpler, albeit slightly less efficient solution.
{🚀} Tip: useEffect and observables
useEffect
可用于设置需要发生的副作用,这些副作用与 React 组件的生命周期绑定。使用 useEffect
需要指定依赖。对于 MobX,这并不是真正需要的,因为 MobX 已经有一种自动确定效果 autorun
的依赖的方法。幸运的是,组合 autorun
并使用 useEffect
将其耦合到组件的生命周期非常简单:
¥useEffect
can be used to set up side effects that need to happen, and which are bound to the life-cycle of the React component.
Using useEffect
requires specifying dependencies.
With MobX that isn't really needed, since MobX has already a way to automatically determine the dependencies of an effect, autorun
.
Combining autorun
and coupling it to the life-cycle of the component using useEffect
is luckily straightforward:
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
得到清理!
¥Note that we return the disposer created by autorun
from our effect function.
This is important, since it makes sure the autorun
gets cleaned up once the component unmounts!
依赖数组通常可以留空,除非不可观察的值应该触发自动运行的重新运行,在这种情况下,你需要将其添加到那里。为了让你的 linter 满意,你可以将 timer
(在上面的示例中)定义为依赖。这是安全的并且不会产生进一步的影响,因为引用实际上永远不会改变。
¥The dependency array can typically be left empty, unless a non-observable value should trigger a re-run of the autorun, in which case you will need to add it there.
To make your linter happy, you can define timer
(in the above example) as a dependency.
That is safe and has no further effect, since the reference will never actually change.
如果你想明确定义哪些可观察量应触发该效果,请使用 reaction
而不是 autorun
,除此之外模式保持相同。
¥If you'd rather explicitly define which observables should trigger the effect, use reaction
instead of autorun
, beyond that the pattern remains identical.
如何进一步优化我的 React 组件?
¥How can I further optimize my React components?
查看 React 优化 {🚀} 部分。
¥Check out the React optimizations {🚀} section.
故障排除
¥Troubleshooting
帮助!我的组件没有重新渲染...
¥Help! My component isn't re-rendering...
确保你没有忘记
observer
(是的,这是最常见的错误)。¥Make sure you didn't forget
observer
(yes, this is the most common mistake).验证你想要做出反应的事物确实是可观察到的。如果需要在运行时验证这一点,请使用
isObservable
、isObservableProp
等实用程序。¥Verify that the thing you intend to react to is indeed observable. Use utilities like
isObservable
,isObservableProp
if needed to verify this at runtime.检查浏览器中的控制台日志是否有任何警告或错误。
¥Check the console logs in the browsers for any warnings or errors.
确保你了解跟踪的一般工作原理。查看 了解反应性 部分。
¥Make sure you grok how tracking works in general. Check out the Understanding reactivity section.
请阅读上述常见陷阱。
¥Read the common pitfalls as described above.
配置 MobX 警告你机制使用不当并检查控制台日志。
¥Configure MobX to warn you of unsound usage of mechanisms and check the console logs.
使用 trace 验证你是否订阅了正确的内容,或者使用 spy / mobx-log 包检查 MobX 一般在做什么。
¥Use trace to verify that you are subscribing to the right things or check what MobX is doing in general using spy / the mobx-log package.