Mobx: MobX roadmap

Created on 26 Apr 2016  Â·  35Comments  Â·  Source: mobxjs/mobx

These are my current ideas of the MobX roadmap (excluding minor improvements), feel free to add your suggestions!

2.2

  • [x] introduce intercept, the counterpart of observe to be able to intercept changes before they are applied. A bit similar to proxy traps. Enables #182, cc: @capaj
  • [x] introduce @action for automatic transactions and tracability + improved logging. Implements #193 and see screenshot below
  • [x] introduce an opt-in strict mode, which forbids changing state outside @action (similar to how it can be forbidden already now in React.render by utilizing the undocumented method mobx.extras.allowStateChanges(boolean, fn))
  • [x] introduce observe(() => expr, value => effect) or similar alternative to autorun?
  • [x] introduce asMap

action-decorator

beyond 2.2

  • [ ] provide standardized time travelling mechanism
  • [ ] provide standardized context / connect mechanism in mobx-react
  • [ ] provide standardized (de)serialization mechanism, see #206
  • [ ] introduce derived arrays, #166 (and for fun built a VDom with it :8ball:)
  • [ ] improve modifiers mechanism? see #211
  • [ ] support onBecome(Un)Observed events

Most helpful comment

Sure, very quick summary:

  • action(name? fn) or @action decorator; use this to mark all your actions for better dev experience, automatic transactions etc. if mobx.useStrict(true) is called, you are forced to use actions if you want to modify state.
  • intercept has basically the same signature as mobx.observe, namely intercept(target, propName?, callback: (change) => change), and is called before changes to the target object are applied. You can return the same change to apply it, no change to ignore it, throw an error, or modify the change object if you want to alter the change. For example to normalize a hex color: intercept(theme, 'backgroundColor', change => { if (change.newValue[0] !== '#') { change.newValue = '#' + change.newValue }; return change })
  • spy(callback: change => void). Fires for each (trans / re)action, state change, computation etc. Is used by the new devtools for example, but it should also be trivial to build time travelling using spy.
  • reaction(expression: () => T, sideEffect: T => void, fireImmediately = false, debounce = 0), sugar for computed(expression).observe(action(sideEffect)) or autorun(() => action(sideEffect)(expression). Expression is tracked, sideEffect is untraced. Unlike autorun, by default the first invocation of the side effect is skipped.

All 35 comments

I liked all the features 🎉

One thing I'd love is an official API for running a callback when an observable starts being observed. I didn't have much luck with atoms and hooking into $mobx.values feels a bit wrong. This would enable lazily populating datastores from a server or localstorage as they get used in views.

@danieldunderfelt isn't that what autorun does?

@AriaFallah No, autorun observes an observable value. I'm talking about detecting when a view, like autorun, starts observing a value :D

So the mere act of writing

autorun(() => {
  const observableValue = observableStore.value
})

will register the autorun function as an observer to observableStore.value. I'd like to hook into that registration "event". It is already possible by adding a function to observableStore.$mobx.values.value.onBecomeObserved, but something like onBecomeObserved(observableStore.value, (observers) => { console.log("value is now being observed by one or more observers") }) would be very cool indeed.

Edit: and yeah, support for computed values would be awesome too!

@danieldunderfelt I remember that we talked about this in the past indeed. Atom's should provide the hooks for this, did you have any luck with those? Or do you want to have it more general, like on normal observable values? Thats a pretty cool & feasible idea now that I think of it :)

@mweststrate A more general approach would be nice! The atoms were a bit clunky, at least as explained in the docs. One of the coolest things in mobx is that simply using a value has an effect, but there's no easy way to subscribe to that "when a view reads a value" event.

I'd like to see more examples which show the idiomatic way to solve different problems (routing, requests and so on). I think currently many examples solve these problems slightly different.

@donaldpipowitch here is my approach for HTTP requests:

https://gist.github.com/hnordt/9674fc5a5a403bc377033f7b9b75453d

@donaldpipowitch @hnordt
A very simple approach for "inline fetching":
https://gist.github.com/otbe/52a5ba1de9264dc1d58c2f21fee338ca
(works very well for simple use cases)

A more complex approach:
https://gist.github.com/otbe/50e3bdfbba2cf0cd1c7fe21008298c15
All components that show some data from a single todo or a list of todos, will show the same version. Independent of when the last loadTodo or loadTodoshappens. Independent of where the call was made.

Maybe this is the wrong place to discuss "best practices", but like @donaldpipowitch said we definitely need a place for this kind of stuff :)

Definitely, I've now thrown most stuff under #104 or under the relevant questions. Does anybody know a nice way to set up a nice list with best practices? Github wiki page?

I guess I wouldn't use wiki pages, but either a separate repository (like Cycle does with https://github.com/cyclejs/examples) or just a folder examples inside this directory. (However I'd place README.md inside every example - Cycle doesn't really explain them.)

As a counter example https://github.com/vitaly-t/pg-promise/wiki uses wiki pages, and it's one of the most helpful resources I've ever used.

@hnordt @otbe Thanks for sharing.

I like intercept and @action

For the strict mode, it might be interesting to have some granularity. For example, depending of the nature of a given store, one might want to enforce it for this store alone. A @strict class decorator might be handy to do just that.

Updated the roadmap above in the most likely order of execution. For 2.2 mainly documentation is left :)

provide standardized context / connect mechanism in mobx-react

@otbe and I currently use https://github.com/inversify/InversifyJS to inject stores in components and we're very happy with that and its a solution which doesn't dependent on React or MobX.

I feel like there could be some improvement in the API for observable objects. In the docs observable section, this is stated:

Properties that are added to the object at a later time won't become observable, unless extendObservable is used.

I think I've read most of what is out there on MobX, including that documentation at one point, and it still didn't sink in. The main message I kept hearing is that if you make something observable, whatever is observing it will be updated if that object changes. And that was my experience, until I tried to add keys to an observable object.

It took a couple hours to hunt down the bug because I didn't suspect MobX's as the problem and it typically works very well. I found that extendObservable will do it, but it took another hour to realize that autorun won't update with properties added with extendObservable. That's unintuitive. mobx.map is apparently the answer, but there are few examples and I still couldn't make it work in a store constructed with a class. Finally I made it work with this:

  @observable someObject = {}

  update(key, val) {
    this.someObject = {...this.someObject, [key]: val}
  }

It works, but it goes against what I understand to be the MobX approach - you can go back to regular, mutable operations (instead of immutable/redux) and MobX will take care of it for you.

So, this could be seen as a problem with the documentation, but to me it really sticks out as an unintuitive approach given the rest of the library. I realize it may be technically challenging, but if there was a way to make observing dynamic keys automatically just work it seems like it would remove a lot of subtle bugs and confusion by MobX developers.

Worst case, if it can't be done then I would suggest making mobx.map very clearly part of the core API and use examples of it.

Overall, great library and except for this I've enjoyed using it. Thank you.

@jefffriesen

From what I know about MobX, I'm actually surprised your update solution works. Anyways, I agree that map should be made a lot more visible than it currently is since it's tucked away in the "Advanced API" section that no one really visits/sees I think, but I have to disagree about the dynamic by default.

Also you said

mobx.map is apparently the answer, but there are few examples and I still couldn't make it work in a store constructed with a class.

What problems did you have?

I tried inside an observable class:

constructor() {
  autorun(() => console.log('someObject: ', this.someObject))  // Only runs once during instantiation
}

@observable someObject = mobx.map({})
// someObject = mobx.map({})   // also doesn't work

update(key, val) {
  this.someObject.set(classification, words)  // Does not trigger `autorun`
}

@AriaFallah I'm curious why you don't think added keys should automatically be observed. Performance? Or do people use that as a feature?

@jefffriesen

As for why your autorun doesn't work you can look at this explanation I wrote on the quirks of autorun:

https://github.com/mobxjs/mobx/issues/248#issuecomment-218927070

Specifically:

With MobX maps to log the whole thing on change it'd be

const x = map({})
autorun(() => console.log(x.toJs()))
x.set('x', 1)
x.set('y', 2)

Also I believe this syntax

@observable someObject = mobx.map({})

is wrong. Use the one you commented out.


Also now that I think about it some more, I don't think I'm super against added keys being observed by default. I guess the only reason I might have disagreed before is the semantics of "this object will never change" with observable and "this object is dynamic" with map.

@AriaFallah That is a good, thorough explanation of autorun. Thanks.

I wonder if it's possible to abstract that complexity away from the user, so autorun just works naively as it's expected to. That's also what I was going for with auto-observing dynamic keys - seems to me like it should just happen for the end user automatically.

@jefffriesen I think that autorun as good as it can be, but the special cases such as logging an entire map for example aren't well explained.

The challenge with that is getting it to work lazily. The laziness is a great feature because it makes sure autorun doesn't run extra times when the object isn't being observed anymore. Though this works against you if you want to log the whole thing because if you don't call some method on your object inside the autorun that reports some piece of it as observed, because of the laziness, the autorun won't run.

I think the solution is perhaps have the toString() method of the object report as observed or something like that, but I'm not sure if it'd work. For now the best thing is just to inform people how autorun actually works because it's not super complex I think.

@jefffriesen @AriaFallah Thanks guys, this is really useful feedback!

@jefffriesen your suspicion is indeed correct, new keys won't be picked up because ES5 has no general construction in which this can be detected (ES6 proxies solves this, but are not available yet in all browsers)

That being said, the situation can be improved I think, here is a short proposal:

  1. Move map indeed to the core api
  2. Introduce asMap modifiers, so that one can do: @observable data = asMap(stuff), var data = observable(asMap(stuff)), which might(?) be more intuitive as it leverages the observable api better.
  3. Introduce additional functions for observable _objects_: keys(), has(propName) toJS(), delete(propName), put(propName, value) (sugar for extendObservable({ propName: value})), merge...?. Especially the keys() is important here as that can be a reactive operation that can be used in loops in autoruns to pick up new keys.
  4. The difficult questions for 3. is where to put those methods? Preferably not on the observable object itself, as they might be overwritten by the objects own methods / values, which might lead to confusing result. Maybe in some objectutils namespace in MobX?

Small example of this proposal:

import {observable, objectutils, autorun} from "mobx"

const myObject = observable({ a: 1, b: 2})

autorun(() => {
  objectutils.entries(myObject).forEach((key, value) => console.log(key, value))
})
// prints: a 1 b 2

objectutils.put(myObject, "c", 3)
// prints a1 b 2 c 3

objectutils.delete(myObject, "a")
// prints b 2 c 3

@mweststrate

I really like the asMap idea because it definitely fits in much better with the decorators and is named much better than just map.

As for the objectutils, is it that you're trying to make regular objects like maps? I don't think it's a bad idea, but when would it be an appropriate case to use the objectutils over asMap?

Wow, proxies as polyfills is not straightforward. You must be paying attention to that because I imagine they would be super helpful for MobX. There's this: https://github.com/GoogleChrome/proxy-polyfill but it doesn't support dynamic keys, unless you just replace the whole object with the key added (which is what I did in my example above).

I still think it would be nice not to have to treat objects differently, but if we have to then asMap looks pretty good. You mentioned moving map into the core API - would you keep both map and asMap or deprecate map? It would be nice to only have asMap if possible.

asMap as a name is more obvious what it's doing and it doesn't collide with map being used other ways such as import {map} from 'lodash' or confused with [].map()....

objectUtils.put() works ok, but it's a little odd. Although I like asMap, what if it was like this:

@observable data = OMap(stuff)  // Observable Map

OMap.put(myObject, "c", 3)

Perhaps proxies could be used if they are supported in browser to provide a warning that adding keys feature is not supported due to proxies not being in all environments.

This is a really interesting idea. MobX needs to work well in lots of browsers, so it can't use Proxies. But developers are the target for this warning, and they use a limited set of browsers. Anecdotally, all people I know develop in Chrome. I am sure there are plenty of folks who develop in Firefox and more and more, IE Edge. I don't have a sense of Safari as the primary dev browser, but it's not supported (Tech Preview it is). Mobil is not supported, but developers typically just check to see if it works in mobile, but do their primary development in a desktop browser.

Here's a list: https://kangax.github.io/compat-table/es6/

I still like creating a better API for this, but this could be a nice addition to that.

@jefffriesen warnings would definitely be intended for developers and could save a bit of time if one creates a new property instead of extendObservable or using map.

One would probably want the check to be removed for NODE_ENV==='production' for perf reasons.

Ok, I figured that asMap should be part as 2.2.0 as well, as it makes the api more consistent.

Besides that, the rest of 2.2 can already be tested with npm install [email protected]

@mweststrate awesome! Any docs/suggestions on what to use?

Sure, very quick summary:

  • action(name? fn) or @action decorator; use this to mark all your actions for better dev experience, automatic transactions etc. if mobx.useStrict(true) is called, you are forced to use actions if you want to modify state.
  • intercept has basically the same signature as mobx.observe, namely intercept(target, propName?, callback: (change) => change), and is called before changes to the target object are applied. You can return the same change to apply it, no change to ignore it, throw an error, or modify the change object if you want to alter the change. For example to normalize a hex color: intercept(theme, 'backgroundColor', change => { if (change.newValue[0] !== '#') { change.newValue = '#' + change.newValue }; return change })
  • spy(callback: change => void). Fires for each (trans / re)action, state change, computation etc. Is used by the new devtools for example, but it should also be trivial to build time travelling using spy.
  • reaction(expression: () => T, sideEffect: T => void, fireImmediately = false, debounce = 0), sugar for computed(expression).observe(action(sideEffect)) or autorun(() => action(sideEffect)(expression). Expression is tracked, sideEffect is untraced. Unlike autorun, by default the first invocation of the side effect is skipped.

N.B: 2.2 has been released yesterday: https://medium.com/@mweststrate/mobx-2-2-explicit-actions-controlled-mutations-and-improved-dx-45cdc73c7c8d

Is impossible or hard to implement the map properties access without the get function? Within some tests that I have done if you access a map's property directly (someMap.name) you get an ObservableMap@X. Am I doing something wrong? 😅

Actually it is possible to merge map and objects and both give them the
same behavior. The convenient thing about map is however that it can has
it's own methods as fields are always accessed through "get". So it is safe
to introduce methods like "keys", "merge", "observe" etc etc without
collision risk. If object and map would be merged, then all those methods
need to become static ( mobx.keys(object), mobx.merge(object, object2),
mobx.put("a", "b") ) etc. Not sure whether that is nice, because it
pollutes the "mobx" namespace. Also, since adding properties in itself is
not observable people have to remember to use mobx.object.put(obj, "a", 3)
when introducing "a", while doing an update object.a = 4 is fine. So that
might be confusing.

Op zo 29 mei 2016 om 22:35 schreef Samuel Simões [email protected]:

Is impossible or hard to implement the map properties access without the
get function? Within some tests that I have done if you access a map's
property directly (someMap.name) you get an ObservableMap@X. I'm doing
something wrong? 😅

—
You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
https://github.com/mobxjs/mobx/issues/219#issuecomment-222381258, or mute
the thread
https://github.com/notifications/unsubscribe/ABvGhBjYgv65XXhJaJDR7ysPM3BGLHs_ks5qGfiEgaJpZM4IP26k
.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

cafreeman picture cafreeman  Â·  4Comments

joey-lucky picture joey-lucky  Â·  3Comments

giacomorebonato picture giacomorebonato  Â·  3Comments

kirhim picture kirhim  Â·  3Comments

etinif picture etinif  Â·  3Comments