@observer
July 21, 2020 ยท View on GitHub
The observer HoC / decorator subscribes React components automatically to any observables that are used during render.
As a result, components will automatically re-render when relevant observables change.
But it also makes sure that components don't re-render when there are no relevant changes.
As a result MobX applications are in practice much better optimized than Redux or vanilla React applications are out of the box.
observeris provided through the separatemobx-reactpackage.- If your code base doesn't have any class based components, you can also the
mobx-react-litepackage, which is smaller.
observer automatically tracks observables used during render
Using observer is pretty straight forward:
import { observer } from "mobx-react"
var timerData = observable({
secondsPassed: 0,
})
setInterval(() => {
timerData.secondsPassed++
}, 1000)
// A hooks based component wrapped with `timerData` will react to changes automatically
const Timer = observer(({ timerData }) => <span>Seconds passed: {timerData.secondsPassed} </span>)
// Alternatively, a class based component:
@observer
class Timer extends React.Component {
render() {
return <span>Seconds passed: {this.props.timerData.secondsPassed} </span>
}
}
ReactDOM.render(<Timer timerData={timerData} />, document.body)
Because observer automatically tracks any observables that are used (and none more), the Timer component above will automatically re-render whenever timerData.secondsPassed is updated, since it is declared as an observable.
Note that observer only subscribes to observables used during the own render of the component. So if observables are passed to child components, those have to be marked as observer as well. This also holds for any callback based components.
observer listens to any observables used
In above components the timer data is received as prop, but in principle that is irrelevant for the working for observer.
Rewriting the Timer component above to the following results in semantically the same application:
const timerData = observable(/* ... */)
const Timer = observer(() => <span>Seconds passed: {timerData.secondsPassed} </span>)
Note that the timerData is now read directly from the closure.
This is a practice we don't recommend, but is shown to demonstrate that observer tracks any observables used, regardless where they are coming from.
Using context to pass observables around
This means that it also possible to store observables in context and use them, and observer will still do its job:
const TimerContext = React.createContext()
const Timer = observer(() => {
const timerData = useContext(TimerContext)
return <span>Seconds passed: {timerData.secondsPassed} </span>
})
const timerData = observable(/* ... */)
const App = () => (
<TimerContext.Provider value={timerData}>
<Timer />
</TimerContext.Provider>
)
Storing observables in local component state
Similarly, we can store observables in local component state using useState. Although in practice local component state is often simple enough to not really need observables. Note that we don't need the state setter since we will mutate the observable, rather than replacing it entirely:
const Timer = observer(() => {
const [timerData] = useState(() =>
observable({
secondsPassed: 0,
})
)
useEffect(() => {
const handle = setInterval(() => {
timerData.secondsPassed++
}, 1000)
return () => {
clearTimeout(handle)
}
}, [])
return <span>Seconds passed: {timerData.secondsPassed} </span>
})
- Tip: the
useLocalStorehook further simplifies this pattern. - Tip: if for some reason
propsor non-observable local state needs to be synced with the observable state, theuseObservableSourcehook can be used.
In general we recommend to not resort to using MobX observables for local component state too quickly; as this can theoretically lock you out of some features of React's Suspense mechanism. Generally speaking it only adds real value when there are complex computations involved (which MobX will optimize) or when there are complex view models.
Local observable state in class based components
Just like normal classes, you can introduce observable properties on a component by using the @observable decorator.
Hence the example above could also have been written as:
@observer
class Timer extends React.Component {
@observable secondsPassed = 0
componentWillMount() {
setInterval(() => {
this.secondsPassed++
}, 1000)
}
render() {
return <span>Seconds passed: {this.secondsPassed} </span>
}
}
For more advantages of using observable local component state, for class based components see 3 reasons why I stopped using setState.
When to apply observer?
The simple rule of thumb is: all components that render observable data.
If you don't want to mark a component as observer, for example to reduce the dependencies of a generic component package, make sure you only pass it plain data, for example by converting it to plain data in a parent component, that is an observer, using toJS.
Characteristics of observer components
- Observer enables your components to interact with state that is not managed by React, and still update as efficiently as possible. This is great for decoupling.
- Observer only subscribe to the data structures that were actively used during the last render. This means that you cannot under-subscribe or over-subscribe. You can even use data in your rendering that will only be available at later moment in time. This is ideal for asynchronously loading data.
- You are not required to declare what data a component will use. Instead, dependencies are determined at runtime and tracked in a very fine-grained manner.
@observerimplementsmemo/shouldComponentUpdateautomatically so that children are not re-rendered unnecessary.observerbased components sideways load data; parent components won't re-render unnecessarily even when child components will.- The props object and the state object of an observer component are automatically made observable to make it easier to create @computed properties that derive from props inside such a component.
Tips
Use the <Observer> component in cases where you can't use observer
Sometimes it is hard to apply observer to a part of the rendering, for example because you are rendering inside a callback, and you don't want to extract a new component to be able to mark it as observer.
In those cases <Observer /> comes in handy. It takes a callback render function, that is automatically rendered again every time an observable used is changed:
const Timer = ({ timerData }) => (
<div>
Seconds passed:
<Observer>{() => <span>{timerData.secondsPassed} </span>}</Observer>
</div>
)
How can I further optimize my React components?
See the relevant React performance section.
Using observer with classes without decorators
Using @observer as a decorator is optional, const Timer = observer(class Timer ... { }) achieves exactly the same.
How to enable decorators?
See also the syntax guide
When combining observer with other higher-order-components, apply observer first
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.
Gotcha: dereference values inside your components
MobX can do a lot, but it cannot make primitive values observable (although it can wrap them in an object see boxed observables).
So not the values that are observable, but the properties of an object. This means that @observer actually reacts to the fact that you dereference a value.
So in our above example, the Timer component would not react if it was initialized as follows:
React.render(<Timer timerData={timerData.secondsPassed} />, document.body)
In this snippet just the current value of secondsPassed is passed to the Timer, which is the immutable value 0 (all primitives are immutable in JS).
That number won't change anymore in the future, so Timer will never update. It is the property secondsPassed that will change in the future,
so we need to access it inside the component. One could also say: values need to be passed by reference and not by value.
If the problem is not entirely clear, make sure to study what does MobX react to? first!
Advanced interaction patterns with reactions, observables, props, etc
In general we recommend to keep UI state and domain state clearly separated,
and manage side effects etc either outside the components using the tools that MobX provides for them (when, flow, reaction etc),
or, if side effects operate on local component state, use the React primitives like useEffect.
Generally it is the clearest to keep those things separated, however, in case you need to cross the boundaries,
you might want to check mobx-react.js.org for pattern on how to connect the two frameworks in more complicated scenario's.
Troubleshooting
- Make sure you didn't forget
observer(yes, this is the most common mistake) - Make sure you grok how tracking works in general: what will MobX react to
- Read the common mistakes section
- Use trace to verify that you are subscribing to the right things or check what MobX is doing in general using spy / the mobx-logger package.