Common pitfalls & best practices
Stuck with MobX? This section contains a list of common issues people new to MobX might run into.
Importing from wrong location
Because MobX ships with typescript typings out of the box, some import autocompleting tools (at least in VSCode) have the habit of auto completing with a wrong import, like
// wrong
import { observable } from "mobx/lib/mobx"
This is incorrect but will not always immediately lead to runtime errors. So be aware. The only correct way of importing anything from the mobx
package is:
// correct
import { observable } from "mobx"
Issues with decorators?
For setup tips and limitations on decorators, check the decorators page
Array.isArray(observable([1,2,3])) === false
In ES5 there is no way to reliably inherit from arrays, and hence observable arrays inherit from objects.
This means that regularly libraries are not able to recognize observable arrays as normal arrays (like lodash, or built-in operations like Array.concat
).
This can simply be fixed by passing calling observable.toJS()
or observable.slice()
before passing the array to another library.
As long as the external library has no intent to modify the array, this will further work completely as expected.
You can use isObservableArray(observable)
to check whether something is an observable array.
object.someNewProp = value
is not picked up
MobX observable objects do not detect or react to property assignments that weren't declared observable before.
So MobX observable objects act as records with predefined keys.
You can use extendObservable(target, props)
to introduce new observable properties to an object.
However object iterators like for .. in
or Object.keys()
won't react to this automatically.
If you need a dynamically keyed object, for example to store users by id, create observable _map_s using observable.map
.
For more info see what will MobX react to?.
Use @observer
on all components that render @observable
s.
@observer
only enhances the component you are decorating, not the components used inside it.
So usually all your components should be decorated. Don't worry, this is not inefficient, in contrast, more observer
components make rendering more efficient.
Don't copy observables properties and store them locally
Observer components only track data that is accessed during the render method. A common mistake is that data plucked of from an observable property and stored will for that reason not be tracked:
class User {
@observable name
}
class Profile extends React.Component {
name
componentWillMount() {
// Wrong
// This dereferences user.name and just copies the value once! Future updates will not be tracked, as lifecycle hooks are not reactive
// assignments like these create redundant data
this.name = this.props.user.name
}
render() {
return <div>{this.name}</div>
}
}
The correct approach is either by not storing the values of observables locally (obviously, the above example is simple but contrived), or by defining them as computed property:
class User {
@observable name
}
class Profile extends React.Component {
@computed get name() {
// correct; computed property will track the `user.name` property
return this.props.user.name
}
render() {
return <div>{this.name}</div>
}
}
Render callbacks are not part of the render method
Because observer
only applies to exactly the render
function of the current component; passing a render callback or component to a child component doesn't become reactive automatically.
For more details, see the what will Mobx react to guide.
Dereference values as late as possible
MobX can do a lot, but it cannot make primitive values observable (although it can wrap them in an object see boxed observables).
So it is 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 in the component. Or in other words: always try to pass the owning object of an observable property.
For more info see what will MobX react to?.
Computed values run more often than expected
If a computed property is not in use by some reaction (autorun
, observer
etc), computed expressions will be evaluated lazily; each time their value is requested (so they just act as normal property).
Computed values will only track their dependencies if they are observed.
This allows MobX to automatically suspend computations that are not actively in use.
See this blog or issue #356 for an explanation.
So if you fiddle arounds, computed properties might not seem efficient. But when applied in a project that uses observer
, autorun
etc, they become very efficient.
MobX computeds will automatically be kept alive during transactions as well, see PRs: #452 and #489
Always dispose reactions
All forms of autorun
, observe
and intercept
will only be garbage collected if all objects they observe are garbage collected themselves.
So it is recommend to use the disposer function that is returned from these methods to stop them when you no longer need them.
Usually for observe
and intercept
it is not strictly necessary to dispose them if when targed this
.
For reactions like autorun
it is more tricky, as they might observe many different observables, and as long as one of them is still in scope,
the reaction will remain in scope which means that all other observables it uses are also kept alive to support future recomputions.
So make sure to always dispose your reactions when you no longer need them!
Example:
const VAT = observable(1.20)
class OrderLIne {
@observable price = 10;
@observable amount = 1;
constructor() {
// this autorun will be GC-ed together with the current orderline instance
this.handler = autorun(() => {
doSomethingWith(this.price * this.amount)
})
// this autorun won't be GC-ed together with the current orderline instance
// since VAT keeps a reference to notify this autorun,
// which in turn keeps 'this' in scope
this.handler = autorun(() => {
doSomethingWith(this.price * this.amount * VAT.get())
})
// So, to avoid subtle memory issues, always call..
this.handler()
// When the reaction is no longer needed!
}
}
I have a weird exception when using @observable
in a React component.
The following exception: Uncaught TypeError: Cannot assign to read only property '__mobxLazyInitializers' of object
occurs when using a react-hot-loader
that does not support decorators.
Either use extendObservable
in componentWillMount
instead of @observable
, or upgrade to react-hot-loader
"^3.0.0-beta.2"
or higher.
The display name of react components is not set
If you use export const MyComponent = observer((props => <div>hi</div>))
, no display name will be visible in the devtools.
The following approaches can be used to fix this:
// 1 (set displayName explicitly)
export const MyComponent = observer((props => <div>hi</div>))
myComponent.displayName = "MyComponent"
// 2 (MobX infers component name from function name)
export const MyComponent = observer(function MyComponent(props) { return <div>hi</div> })
// 3 (transpiler will infer component name from variable name)
const _MyComponent = observer((props => <div>hi</div>)) //
export const MyComponent = observer(_MyComponent)
// 4 (with default export)
const MyComponent = observer((props => <div>hi</div>))
export default observer(MyComponent)
See also: http://mobxjs.github.io/mobx/best/stateless-HMR.html or #141.
The propType of an observable array is object
Observable arrays are actually objects, so they comply to propTypes.object
instead of array
.
mobx-react
provides its explicit PropTypes
for observable data structures.
Rendering ListViews in React Native
ListView.DataSource
in React Native expects real arrays. Observable arrays are actually objects, make sure to .slice()
them first before passing to list views. Furthermore, ListView.DataSource
itself can be moved to the store and have it automatically updated with a @computed
, this step can also be done on the component level.
class ListStore {
@observable list = [
'Hello World!',
'Hello React Native!',
'Hello MobX!'
];
ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
@computed get dataSource() {
return this.ds.cloneWithRows(this.list.slice());
}
}
const listStore = new ListStore();
@observer class List extends Component {
render() {
return (
<ListView
dataSource={listStore.dataSource}
renderRow={row => <Text>{row}</Text>}
enableEmptySections={true}
/>
);
}
}
For more info see #476
Declaring propTypes might cause unnecessary renders in dev mode
See: https://github.com/mobxjs/mobx-react/issues/56
@observable
properties initialize lazily when using Babel
This issue only occurs when transpiling with Babel and not with Typescript (in which decorator support is more mature). Observable properties will not be instantiated upon an instance until the first read / write to a property (at that point they all will be initialized). This results in the following subtle bug:
class Todo {
@observable done = true
@observable title = "test"
}
const todo = new Todo()
"done" in todo // true
todo.hasOwnProperty("done") // false
Object.keys(todo) // []
console.log(todo.title)
"done" in todo // true
todo.hasOwnProperty("done") // true
Object.keys(todo) // ["done", "title"]
In practice this is rarely an issue, only when using generic methods like Object.assign(target, todo)
or assert.deepEquals
before reading or writing any property of the object.
If you want to make sure that this issue doesn't occur, just initialize the fields in the constructor instead of at the field declaration or use extendObservable
to create the observable properties.