Hacker News new | past | comments | ask | show | jobs | submit login
MobX 6 (michel.codes)
162 points by nikivi on Oct 1, 2020 | hide | past | favorite | 86 comments



MobX is possibly my favorite open-source project. The elegance with which it makes an entire problem-space just disappear, with minimal magic or surprises, with maximal performance, with a delightful user-experience, with minimal opinions or assumptions about how you use it. I have nothing but praise; React + MobX is the most productive I've ever been while working on user interfaces.

On top of all that, Michael is a very reasonable and patient project leader. His documentation puts advanced concepts in plain terms, and when people file feature-requests he always displays a genuine willingness to consider their use-cases and points of view, to solve their problem if it's reasonable for the project to do so, and is never condescending even when questions or suggestions are plainly bad.


I have mostly found it to be a footgun and inspiration to write spaghetti. While it may have good intentions, it doesn't matter if it lets less experienced developers do absurd things that I now have to refactor. I don't think it really fits well into the React model, and while it's not only used for React, it's quite common that way


> I have mostly found it to be a footgun and inspiration to write spaghetti. While it may have good intentions, it doesn't matter if it lets less experienced developers do absurd things that I now have to refactor.

I admit the only team I've used it on consisted of three people, and there are certainly really gnarly things you can do with it if you go out of the way to, so I don't know how it scales to larger teams. Though I would argue that it encourages best practices by making those the paths of least resistance. I would also say that - unlike, say, RxJS - its mental model is very simple and straightforward, and should be quite accessible to inexperienced developers. But yes, it doesn't do a lot to stop you from shooting yourself in the foot.

> I don't think it really fits well into the React model

I disagree with this part completely. It doesn't fit well with the Redux/hooks model, but that's because it replaces it. In my opinion it lets React focus on what it does best: updating the DOM to match a new virtual DOM, and takes everything else out of React's hands, which to me is a huge win. I am not a believer in the "every state change replaces the entire root state structure using a composition of pure functions" model; I think it's contrived, clunky, and hard to follow. I believe that state should be minimal, but should be treated as what it is. MobX gives you all the tools you need to enshrine state as state, without introducing any duplication/synchronization complications.


My experience has been the reverse. I've found MobX code easier to maintain than equivalent Redux code, and although it's obviously subjective, I feel it is a good fit for the React model.

Just noting that experiences may differ sharply. :)


Same. I found MobX to be easier to reason about than Redux. I'd really like to give MST (mobx-state-tree) a closer look, too. MWeststrate is a "gentle giant" of sorts, and the idea of leveraging his more-opinionated framework resonates.


I’ve tried to use MST, but found it quite hard and contrived to use. I’m sticking with regular mobx for now, which I absolutely love. Since MST seems to be looking for maintainers I’m inclined to think it’s the best choice.

If you really need the (complex) functionality of MST, redux might be a safer choice going forward in that case.


I've used MST on a project and it was everything I'd hoped. Obviously one project isn't a lot of data, but I'd certainly use it again if another opportunity arose.


It doesn't sound like the MobX author has any attention on supporting MST.


I found this as well. As an example (probably just bad coding on my part) I tried to use mobx to track preferences for a photo viewer, every photo then looked at the preference state from mobx. App slowed to a crawl because 300 photos are going through a few hundred lines of code each just to see if `showDate` is true/false and because `showDate` is "reactive" there's a whole ton of overhead it tracking that each photo is watching it.

Now of course I moved the code that looks at that state up the tree and then pass it down to solve but that kind of defeats the purpose of using it in the first place. The upper components should not have to care directly about data for lower components.


What I would do in that situation is:

1) If all 300 photos aren't actually visible, make sure React skips actually rendering them (calling their render functions)

2) If they do all need to be visible, and they do all need to update in real time in response to that preference, consider adding a paging mechanism so that not as many are visible at once

You have to step back to the root question: do you want all of those components to visibly update immediately in response to that change? If not, make sure they don't try to. If yes, MobX is accomplishing that in the most efficient way possible so you may need to rethink your UI.

> I moved the code that looks at that state up the tree and then pass it down to solve

This doesn't really make sense to me as a fix, and to be honest I suspect it's merely covering up the problem in a roundabout way.


Redux is king speg and boiler plater. So much work to do simple things, followed with advanced tricks to me the difficult things easier.

I've used both extensively, MobX def wins IMO.


> a footgun and inspiration to write spaghetti. While it may have good intentions, it doesn't matter if it lets less experienced developers do absurd things that I now have to refactor

isn't it everything? My exp in redux is the same. Mobx however doesn't pretend to be for smart people and doesn't encourage them to invent a bunch of weird conventions like redux. I've never had any problem with state management, react solved it for me already. What I need a library for is a clear pattern for where to put domain code (again is it reducer or saga/thunk?) and never think about the library again.


Yeah its being used in a project I'm working on and basically is a major footgun. Also not every browser native api works well with it like I was expected to use this with p2p webrtc but it gives out weird "that function does not exist" errors when calling a method from another method.


That kind of error would have nothing to do with MobX. Hard to say more without knowing the specifics.


> React + MobX is the most productive I've ever been while working on user interfaces

I loved using React/Inferno + MobX too.

Right now I find myself being much more productive with Svelte which already includes a reactive primitive out of the box (writable/readable) that can be used with classes too.


I haven't tried Svelte, but I've heard good things. I am curious how it handles reactivity beyond just the UI; the way you can use MobX's computed values for any arbitrary memoization layer is one of my favorite features.


AFAIK Svelte doesn't have memoization because, unlike React, it does't really need it. Svelte tracks reactive dependencies at compile time so that only the necessary parts of the DOM are updated when some state changes.

Svelte has a derived reactive store similar to a computed value in Mobx:

https://svelte.dev/docs#derived


In Svelte you can define a reactive statement (or block) with the $ label syntax. It's semantically the same of a MobX's computed value, expressed succinctly `$: v = expr` where expr will be rerun when and only if its dependencies change.


Sadly, that's only in .svelte files though.


Superb to hear that you are having maximally delightful experiences with this minimally opinionated solution-solving productive platform.


I think you're being sarcastic, but my praise is genuine and not exaggerated. I've seen people express similar praise for projects like Clojure; sometimes you just find something that solves your problem so perfectly that you can't help but go on about it.


problem-space just disappear = magic


It has two key aspects that are very magical: the use of Proxy, and the way "tracked functions" work. But both of these, if you take the time to learn, are not very hard to understand. And once you do, everything else very straightforwardly and non-magically falls out from that. These two bits of magic also don't have many caveats/leaks, so in practice you usually don't have to think about the way they're implemented and can just work with the abstraction.

Compare this with frameworks like Aurelia that bring in piles of new, implicit, inscrutable abstractions in order to accomplish roughly the same thing. Sprawling magic, that you find yourself constantly tripping over left and right.


I'd counter: the right level of abstraction. If you have complex views that rely on a variety of properties, and you want performant updates, mutable change tracking automates the process of making it work at precisely the right level. The problem space that goes away is, in redux terminology, the need to make custom selectors as a performance optimization. Instead, you access the properties you need in your render functions, using vanilla js syntax, and mobx tracks it for you.


I think MobX is amazing for many reasons, but primarily because it makes my life building React and React Native apps so much simpler than the alternatives (I guess I am mainly thinking Redux here as it's the only one I've had significant experience with).

Sure, there are a few gotchas (less so with Proxy object in v5, but there are still some) and you still have to pay some attention to avoiding performance issues, but I think this is true of any alternatives too, and MobX makes 95% of standard React-stateful-app-type dev so easy that I don't mind if there is the odd time where I need to debug something. I think pretty much everyone I've introduced to it has found the same.

As others have alluded to, I feel like often solutions people propose in the world of JS dev (and I'm sure elsewhere) can be overly fussy and complicated, just to achieve some notion of "purity" or whatever. MobX is a pragmatic solution and that aligns with the pragmatic way I generally like to work, so I'm happy to trade off some "magic" for a great developer experience, and I'd really recommend everyone try it at least once!


So mobx is basically global state, and an efficient change tracking algo that decides which parts of the tree need to be re-rendered.

I've found using React context for state, and judicious usage of useMemo has been enough for me to be able to drop mobx or its kin, even on larger sized projects.

Libraries like mobx and redux implicitly promote a massive singleton state object. I think web applications are easier to maintain if we make every effort to minimise global state. If some component way down on the left hand side of the tree needs to get something to a component at say the top RHS of the tree, I will consider refactoring, rather than add another property to MassiveStateObject. Why? Because when you come back to look at MassiveStateObject in two years the implicit coupling between the two components above will not be clear.


Mobx is not a global state, you can create your stores as granular as you want them to.

Each widget can have a store, each section can have a store, each view can have a store, etc. You decicde what works for you.

(I use mobx in a large and complex trading application)


The way that mobx is advised to be used, that is/was documented by its author, promotes the usage of a rootStore, which has as members all the various granular store objects. Is this how your organisation's stores are constructed?


We used to have that but moved to an approach with multiple "rootStores" per self-contained route (sometimes we have more than 1 root store in such a scenario).

But we also have multiple smaller stores which are not connected to the root store. Think: multiple not-connected trees where each node is a store.

I must agree that both approaches have pros and cons, e.g. passing down data down a long path of stores is annoying.


In my experience, not specific to front end, managing multiple long life objects can be a headache.

Out of curiosity, how do your 'not connected' stores know when, say, a user logs out?

Also, I suppose you have to write code, bespoke to each object, to be able to reset these objects too?


(Thinking in terms of local stores)

> how do your 'not connected' stores know when, say, a user logs out?

If they need this information, it can be passed in from above, or pulled from context. But unless the store and component need to stay mounted when user logs out, they might not need to do anything. There's nothing wrong with a local store being created by a component that receives props and provides them via constructor arguments.

> write code, bespoke to each object, to be able to reset these objects too?

I almost never do this, and nearly always create a new store when the component mounts. The combination of `useLocalStore`[1] with mobx-react-lite makes this straight forward. So per the above example, your component could `useLocalObservable(() => new Store(props.user))`

[1]: https://mobx.js.org/react-integration.html


To be honest I don't understand why people use mobx this way. I mean short life store objects, populated by props, that are recreated every mount. Might as well just use React state and save the overhead. You might say, because I have several components using the same business logic and fields and such, I can inject the same store into multiple components. I would say that logic can go in functions which take React state as their argument. You don't need a global state management tool to separate business logic from the view.


> Might as well just use React state and save the overhead. Unless you mean conceptual overhead (i.e another library), it should be less overhead with mobx because there's no need to re-create objects on every re-render, and further re-renders in general are less common (since mobx precisely controls renders for you). Also localStore's need not be short lived by definition, just local. I have a notes app where the main components localStore stays mounted for (literally) days :)


It's more overhead in the sense that mobx needs to walk all your store objects and check for changes, on top of what React does. Anyway that's fine if you really need that precision. I just useMemo at the 'hotspots' and that is enough. Remember also that if vdom diffing doesn't pick up a change, nothing is flushed out to the DOM, so whether an object is created or long-life doesn't matter in this sense.


> mobx needs to walk all your store objects and check for changes, on top of what React does.

I don't think that is quite correct. I think it keeps a mapping of observed properties and which React components rely on them. Then, changing any property becomes a lookup (not a walk) of who is observering, and a `forceRender` on the results. So if no components are observing the property, there's no further computation. Its not "on top of what React does" because it short-circuits it (like useMemo) -- except you don't have to hand-craft any useMemo comparators, it does that for you by tracking which properties you access.


Most of the stores not long-life objects.

The current store holding most of the displayed data stays alive (while not switching to a different route), but everything else resets or gets recreated when needed. (no need to keep stores alive if they are not required or used)

Stores for specific "sub" views (think dialogs, tabs, collapsibles, ...) are getting created/destroyed ad-hoc.

Main Store for a specific view holds the current data (which receives updates via gql subscriptions ~every second), the current filter which is applied to it's children (only show Apple related securities) and currently visible/hidden state of any children.

User/App/Global related attributes which don't change often are stored in a globally available object (and is easy for us as users don't log out or anything)


no, do you have source for that? Mobx is built on the concept of atom which you can put anywhere in your app, be it a module, a context or a component.



I believe having application state defined independently of the UI components is very helpful in separating concerns. I can debug the state of the application as data only, without needing to dive in to the UI components hierarchy. UI is just a representation of that state and there can be more than one such representation. Singleton isn't bad if it is used only for reading the data. Mutable global state is the problem.


I think you are essentially talking about unit testing your stores, and in the case where there might be a lot of logic inside them, is a good idea in principal.

But take the case of an app using local state, this logic if indeed separable from UI, could be coded as functions, rather than within imperative object 'stores'. And these functions would obviously be just as testable.


You mention redux in your original comment and I wouldn't call redux store an imperative object. You have pure functions (reducers) updating a state, which is just a simple object without methods. The benefits I see in using this one application state is that I can easily debug the state of the application, I can easily serialize the whole state and include it in a crash report and the devtools for redux are very helpful. Although YMMV


> Singleton isn't bad if it is used only for reading the data. Mutable global state is the problem.

If it’s read-only, you can just declare it as a constant. No need to add a state management library.


Read-only != constant. State management library can provide you with globally readable state represented by stream of immutable values.


I have come mostly to the same conclusion. UI specific state (eg a flag to store whether the left panel is folded) should be as local as possible.

When you have some functional data (the lisk of tasks in a todo app) then putting that in the React context and injecting it in the UI blocks that need it is the simplest way as long the injection part is well separated (using something like unstated.next).

There is some grey area between the two but with this methodology I have never felt the need to use redux or mobx again.

The last time I had to use mobx I was bitten by the rough edges of the decorators.


UI specific state (eg a flag to store whether the left panel is folded) should be as local as possible

MobX is not a replacement for local state. It's a useful mechanism for sharing state between parts of an app that need to share state - in the example of a left panel, if you want to be able to control that from outside of the component then you either need to be able to modify the state (eg MobX), or you need a callback that refers to the local panel state that's callable from outside (eg a Redux action), or you need a method that's passed around the app to where it's needed (eg a React context). If you don't need to control it from outside then you don't need any of these things and useState is fine.

Where MobX is especially useful is in things that aren't simply an observable value that need to be passed around - if you need to transform the value for other components to use it then MobX's @computed and @action are both _incredibly_ helpful.


In my opinion, the sharing of state in the manner you described leads to implicit coupling between components, which can be hard to hold in your head as the application grows.


I would always forget to put the observable decorator, then wonder why there was not a re-render. This situation occurred so many times I lost count. It's not mobx, it's me that is the problem.


Maybe you'll like this update then -- check out `makeAutoObservable`, it automatically picks sensible defaults to make observable


What do you do with state that needs to be global, or state that bits of start needing to be available in many components for example current username or permissions etc?


This is not a religious stand against all global state. Username, permissions fine to be global, this is very easy for someone new to your code to grok. I put them in the top React context, then just useReducer where I need to. Don't need to pass down through props to every component anymore, thanks to React context and useReducer.

Although I was thinking if you wanted to be a religious zealot about not coding any of your own global state you could use the browser session API.


React Context is the use case for that one.


Hah, Michel got me at the end with the plot twist.

If I could add a feature to MobX I would add a built-in easy way for serializing and deserializing state to local/sessionStorage without having to reach out for 3rd party library (which aren't even that good). I think that functionality is very vital for any web app.

After that, I guess my biggest issue has been the injection of stores into other stores. It feels a bit dirty and can get messy. Maybe if instead of injection, MobX could offer a context based solution. And then nudge people to build their stores around specific API route/data to make them composable and smaller than the gigantic stores they sometimes come to be. Then you could easily separate the part of the store that is about how to fetch/send data to the API and the part that formats/modifies the data for the UI.

I don't know, maybe that could work. Love MobX either way and hope they keep up the good work!


MobX has always been such a practical and down Earth solution, I really appreciate Michel and others contributions to make React development simpler. This version seems to make same practical decisions in light of the resistance to observers and annotations ever getting approved by the standards committees.


Observers are a bad pattern in a long run if you are working on large projects.


I'm curious what makes it bad on large projects.


If you thought goto was bad, observers are basically a comefrom statement. You change something and literally anything can happen. Once code starts getting large, these will start causing chain reactions and the code gets hard to reason about.


The goal is for nearly all of your observers to be nothing but a cache-clear on a pure function ("computed", in MobX's case). Chained reactions (reactions that trigger other reactions) are very rarely necessary and should be kept to a small enough number that you can model the entire state-graph in your head.


MobX works well when paired with a Rx-like library for the more complex cases. With that its never necessary to chain reactions.


In my experience with MobX, reactions that mutate observables (which triggers other reactions) can quickly get out of hand and become hard to debug if you're not careful.

But doing this is also a huge code smell, and expressly stated as such in the docs, and even though it's necessary every once in a while, it's very possible to keep it under control to where it doesn't become a problem even in quite large projects.


We put a facade on top of mobx that requires every autorun, reaction and runInAction to be named. Of course, updates outside of transactions are forbidden. This makes even complex applications debuggable!


That's why it should be forbidden by default. But if you use 1 way flow (events -> store -> ui) then MobX is great.


To some people comparing mobx to react saying that "i just ditched mobx because react does the same thing" I don't believe this is true. Apart from the seperation-of-concerns benefit mobx is different in the sense that it works with observables whereas react works with diffing tree branches.

In other words react needs to work with DOM diffing and virtual dom whereas mobx and observables are granular & directly manipulate the values.

This gives mobx in my opinion lots of benefits that are not so apparent in react. Mainly the benefit of performance where a computed property only reruns if any of its dependencies change whereas react updates when any of its props change, even if the prop wasn't used!

A simple example is imagine in react:

props => if (props.a) bar() else if (props.b) baz()

React doesn't care about a nor b and reruns this code whenever props changes.

In mobx (and other observable based systems) if the above computed function runs with prop.a equal to true will make mobx know that "a" needs to be tracked and mobx doesn't yet know about "b" (because that branch has never been evaluated) hence doesn't rerun if b changes.

This kind of reactive granularity is what sets the two apart imho.

solidjs [1] is a framework that works like mobx but for components; hence seems to be the best combination of mobx + react using observables (without virtual dom diffing)

1. https://github.com/ryansolid/solid


Michel's immutability library immer is also awesome. I loved reading his post[1] about how it worked and digging through the source code. The JS community is lucky to have him and people like him.

[1]: https://hackernoon.com/introducing-immer-immutability-the-ea...


A bastion of sanity against the problem of "how do I update a user interface when I know something changed?"


Any opinions of this library? https://github.com/RisingStack/react-easy-state

It looks like a slimmer version of MobX


It's great!

In terms of concepts it's very similar to MobX but the API is minimal because it's focused on react only.

We're using react-easy-state in the Frontity framework https://github.com/frontity/frontity/ and very happy with it.

(shameless plug warning) I've written about principles behind react-easy-state and MobX here: https://czaplinski.io/blog/make-your-own-mobx/


Seems mainly like an alternative to using class-based stores, which I guess gets under some people's skin.


You will have to take my decorators from my cold dead hands. I'm not upgrading.


It still supports decorators: https://mobx.js.org/enabling-decorators.html


Haha, I had the same reaction when I read the first heading. Then I read on and was so relieved that they are still supported. Such a rollercoaster.


You still needed override constructor with makeObservable(this). This is too far from the grace of decorators. It make me sad.


It was a long time ago I built anything with React but I used to use MobX mostly because of the inject function, to be able to access the global store from any component without prop drilling. I don't see much emphasis on that anymore though. Has React incorporated a native function for that? Is it the context API?

Edit: What I don't understand with the context API is that it looks like I need to access the instantiated context externally in every component, while MobX injected it through the component tree automatically.


yeah I don't understand why the react team made such an API. It's literally service locator an anti-pattern. I started to think maybe it's best not to use mobx nor react for dependency injection and usehigher order functions/components in a poor man's DI manner is the most sustainable way forward. Because you can inverse control everything not just UIs/observables. Or use a separate lib like inversify or aurelia/dependency-injection


As someone who tried react + redux and then recently moved to svelte because the way things were done just made sense, how does mobx compare?


Not used Svelte but knowing how it works I guess MobX is simpler to debug. It has synchronous updates, so debugging is easy by just followig stacktrace. Not sure if Svelte is easily debuggable in contrast as it's compiled.


IMO You don't need mobx if you're using svelte. Use svelte stores.


but then you have domain code that depends on a view library, which defeats the whole point of having a store.


Nice work MW!

I’m now left wondering what sorts of impending doom the decorator-withdrawal-situation will create for NestJS.


For those that haven't tried, I recommend pairing with mobx-state-tree.

https://github.com/mobxjs/mobx-state-tree


I used mobx-state-tree once with great success to implement efficient time travelling in an after effects like editor (to implement cmd+z/cmd+y). In the app, the state was updated multiple times per second and it still performed amazingly. I migrated this from mobx to mobx-state-tree and as both libraries work together seamlessly, the migration was super straight forward.


Yeah MobX State Tree does really bring Mobx to the next level. Have been using it for years and it's amazing to use.


[[define]] semantics for class fields causes problems again. Sigh.


I agree, super annoying, but we'll continue to have problems like these as long as transpilers keep running ahead of the official spec.

In general I think the TC39 commitee has been doing a great job of moving JS forward, so I'm trusting them to have solid reasons for this call, knowing full well that it's painful in the short term.


I think this is entirely on TC39 not on transpilers, as currently there is absolutely no alternative mechanism and the resulting behavior is just surprising to many.



Great work MW! Happy to follow mobx's growth.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: