Why MobX Is No Longer Fashionable
MobX was one of React’s most popular state management libraries, so why is it passé now?
MobX was a popular state management library for React that allowed you to create observable and reactive data models. However, with the introduction of React Hooks, MobX became less appealing and, sometimes, conflicted with React’s philosophy.
In this article, I will explain some of the reasons why MobX is not fashionable with React Hooks and suggest some alternatives that might suit your needs better.
The Observer Trap
One of the core concepts of MobX is that you need to wrap your components with observer to make them react to changes in observable data. This sounds simple enough, but it comes with several drawbacks:
It is very easy to forget to add it. You may start with a pure component without using any reactive object. But later, the component may change, and it is too easy to miss the observer. If you forget to wrap your component with observer, it will not update when the data changes and you might spend hours debugging why your component is not working as expected.
You avoid this pitfall by using eslint-plugin-mobx, but it does not make your life better because of the following:
The wrapped component will break the definition lookup in the editor. If you use an editor that supports code navigation and definition lookup, such as VS Code, you might find that “go to the definition” of observer-wrapped component will give you two locations. One for the actual component definition and the other observer call.
This can be confusing and annoying, especially if your project has many components. Even inlining the component function expression inside the observer will not change this behavior. Plus, it will conflict with the prefer-arrow-callback rule, which recommends using arrow functions for callbacks.
observer must be the innermost (first applied) higher-order-component when it is combined with other decorators or HOCs. Otherwise, it might do nothing at all. What’s more frustrating is that this rule is not easy to check with eslint.
As you can see, using observer can introduce a lot of complexity and potential bugs in your code.
The Global Store Confusion
More confusion happens when you use the global MobX store. A global store is a singleton object that contains all your application’s state and logic and can be imported from any file in your project. You can create a global store using MobX by using the observable function or the @observable decorator.
However, using a global store also has the following drawbacks:
globalStore.reactiveProperty in useEffect will be treated as an unnecessary dependency by the eslint rule react-hooks/exhaustive-deps. If you use a global store property in a useEffect hook, the linter will complain that it is an unnecessary dependency and suggest that you remove it from the dependency array.
But without writing this dependency, useEffect will not be rerun when the store is updated. Why? Because the reactive property must be accessed so that MobX knows the render function or effect function should be retriggered when the property changes. Skipping it will make both MobX and React ignore the property.
A workaround is to use a global store with context API. You can create a context provider that wraps your app component and passes the global store as its value. Then, you can use the useContext hook in any component needing access to the global store. This way, you can suppress exhaustive-deps’ complaints. But you still need to specify useEffect’s dependency array.
You Still Need To Annotate useEffect’s Dependencies
This might seem redundant, as MobX is already a reactive library, but MobX must track the changes in the render function.
Will autorun, the utility function from MobX that re-runs a function whenever an observable value changes, save us? Sadly, the answer is no.
autorun will run multiple times or will capture stale closure. Let’s see the example.
class Store {
num = 123
constructor() { makeAutoObservable(this) }
}
const store = new Store()
function Component({num}) {
useEffect(() => {
console.log('plain effect', store.num, num)
}, [num])
useEffect(() => autorun(() => {
console.log('autorun', store.num, num)
}), [num])
return <button onClick={() => store.num++}>click</button>
}
const Comp = observer(Component)
const Root = observer(() => <Comp num={store.num} />)
render(<Root/>, document.getElementById("root"));
What’s the output? autorun will fire twice, while plain effect will only run once.
The reason for these problems is that MobX’s autorun function does not work well with React’s useEffect hook. The useEffect hook runs only once or when its dependencies change, but the autorun function runs whenever an observable value changes. This means that the autorun function might run before or after the useEffect hook, or it might run multiple times within the same render cycle. This can cause inconsistencies and bugs in your code.
What if we set the dependency array to empty? autorun will always capture the stale closure and num will always be the initial value. So it is not correct either.
Other Pitfalls
Besides the issues mentioned above, there are some other footguns that you might encounter when using MobX with React Hooks.
These foot guns are hidden in the folded tips section of the MobX documentation, and they might surprise you if you are unaware of them. Here are some examples:
- Callback components require observer. If your component renders another component via a callback prop, such as a render prop, you need to wrap the callback component with <Observer> as well. Note, <Observer> and observer are two different concepts. <Observer> is a React component in which the children are React elements in the anonymous region in your component. While observer is a higher-order function that takes a component function.
- Memory leak if you forget to call enableStaticRendering(true).
- Using a third-party component library needs extra attention. If you use a third-party component library incompatible with MobX, such as Material UI or Ant Design, you might need some help using their components with observable data. For example, passing a reactive object as a prop may not trigger a third-party components’ update.
Alternatives
As you can see, using MobX with React Hooks can be tricky and error-prone. Our brains have been trained to remember useEffect as our second instinct. But using MobX will make us rewire our brain. Is it worth it? My answer is no.
For simple cases, plain old React context may be sufficient for your work. For more complex scenarios, using more modern state management like jotai and zustand may be better.
If you are really into reactive programming, Vue is a pretty solid choice :).
Why Mobx Is No Longer Fashionable was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.