-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Improve automatic conversion to observables #649
Comments
This is great! I didn't know about this automagic conversion in past and I had to spend a lot of time investigating what was wrong with my code, ending with forcing object back to plain with a |
What would happen if you do |
only first level |
Sounds good, but maybe it would be nice to have |
probably it should make anything observable that is initially in there indeed |
3.this certainly brings MobX closer to the API we'll have in couple of years with ES6 Proxies. With proxies you won't modify the source object, but you instead generate a proxy object and use that instead of the original. It is a good idea in that regard to leave the source untouched.
warn? Why not go a step further and throw an error? This would make sense if it is perf expensive. I don't think it is because it just checks one property right? |
+1 for observableDeep. Other than that, I do think it's clearer this way. On Nov 12, 2016 5:55 AM, "Artur Eshenbrener" [email protected]
|
Agree, but should we change the API because people don't read the docs? Maybe we can just make it more visible somehow? |
Or have a shallowObservable option? I could go either way. That magic On Nov 12, 2016 7:49 AM, "Jiri Spac" [email protected] wrote:
|
I think |
@andykog I like the idea of |
Basically repeating ideas from #211 const Mobx = require("mobx");
Mobx.Observable.shallow(obj, modifiers) // no default conversions, same as asFlat
Mobx.Observable.deep(obj, modifiers) // same as current observable
Mobx.Observable.computed(obj, modifiers)
Mobx.Observable.extend(obj, propDefinitions) // no default conversions, modifies passed obj Remove Remove Introduce
Yes, but move the explicitness to the left side (decorator/modifier) so the stickyness is apparent I think that better naming alone would solve some issues - these changes are in fact just cosmetics, they don't add or remove features. The thing is that current API doesn't provide any hints about what's going to happen (aside from vague "it makes thing observable") or if there are other options available. As a result, users discover these things by making mistakes, which is not ideal. EDIT: removed |
Hmm.. I'm not sure about This was confusing:
But this is confusing as well!
In that sense, I like @capaj's notion that Proxies allow this to be transparent, and moving in that direction would encourage enhancing. |
@mweststrate, I thought |
Ok, two proposals. Not too many comments, as the API should be self-explanatory Proposal 1
Proposal 2Based on @urugators proposal. Just not sure if
Note that the second proposal introduces
asStructure?Didn't add |
2.seems like a bigger change. I kinda like MobX as it is now and I am a conservative developer when it comes to the tools I hold dear. So for me, 1. seems like a better choice. I am not thrilled by how long the new names are: |
@andykog yeah I have no clear opinion about that yet, I can imagine people might have libraries / architectures relying on that behavior, so it feels it should remain available somehow? Anyway, I got stuck on that with the following:
|
@capaj can you check the numbering in your answer ;-) seems off |
@mweststrate should be fixed now on github. It is probably wrong in the email notification. I should start previewing before posting 🤕 |
Sounds good! I usually use |
Could
So to do a deep observable, you could write:
|
I do believe that libraries should always evolve if necessary, but I am against all of these changes, except number 3 (which I don't think matters much anyway). It's really not hard to avoid these problems if you read the docs. Taking the Observable API from 1 to 4 decorators (Proposal 1) or bringing in a bunch of helper functions like Immutable (Proposal 2) really increase the barrier of entry to this library. One of the things that really sold me on MobX is the simplicity of "it just works". And it REALLY DOES "just work". And when it doesn't work, I read the docs, and there are sane solutions to my problems. As for This library has an amazing and easy to learn API and I'd hate to see that change. |
Thinking about it, having observable arrays make future values observable tracks the behavior of observables objects. For example
If you set f.obj to an object literal, then everything therein will be made observable. If, however, you set it to an instance of a class (or really anything with a prototype) then this is not done, since the assumption is that you will have already handled making what needs to be observable, observable (in your class definition, presumably). Arrays behave the same way. If you add a class instance, it's left alone. If you add an object literal, it's made observable. I'm not sure breaking this symmetry would accomplish you much. |
I agree with @arackaf. I have all my state as an obeservable and have no illusion about what is and isn't observable and if its portions should or shouldn't be observable. I trust in the dependency graph you generate to do its work, and it hasn't let me down once 🌹 |
Edited my original post (removed To Proposal 2: Autoconversion(stickyness) on property could work like this: @deep prop = []; // left side, sticky, autoconvert
@observable prop = Observable.deep([]); // right side, non-sticky, don't autoconvert
// Sticky shallows
@shallow prop = []; // = {}; // = new Map(); |
I know this thread was probably created because maintainers keep having to help people through edges cases. Early on @mweststrate helped me through the problem of dynamically added object keys are not observable (thank you, btw). But I use Mobx quite a bit and that's the only edge case I've personally ran into. There is a proposal for dealing with dynamic keys here #652. But adding to the available number of functions has overhead for the user (and the maintainers). I love that This may be an unpopular idea, but just throwing it out there. Would counting on having ES6 proxies available allow us to have a more streamline API with fewer edge cases? I know it would help when adding keys to observable objects, but my impression is it could help in other ways. It seems like it could at least allow for amazing DX with better warnings. This chart has changed quite a bit since I last looked about 6 months ago: https://kangax.github.io/compat-table/es6/. Proxies are available in the latest major versions of desktop browsers, Node 6+, Electron, and iOS10. The latest Android browser isn't supported - not sure where mobile Chrome is on that list. That support covers a huge set of use cases. My point is that the current Mobx release is great and it can still be used in cases that require broader support. But for people who can depend on proxies, what could we do? From my perspective, the proposals above seem to lose convenience or expand the API with too many options (and unfortunately I don't have any better ideas). API design can make or break a library and I'm concerned about where this could go. I don't know the internals of Mobx so it's hard for me to say how much proxies could help. Thoughts? |
Note that this proposal has a clear way to construct both plain and deep versions for any specific structure: const myMap = observable.map({ a: 1 }) // shallow
const myMap = observable.map({ a: 1}, { deep: true}) // deep
// or optionally from ES6 map, will throw on non-string keys:
const myMap = observable(new window.Map()) Also not that it is no longer possible to give specific fields a specific modifier with Although there are still |
@mweststrate Could you still do this? const obj = observable({
prop1: {a: 1, b: 2},
prop2: observable.ref({a: 1, b: 2})
}); I believe this is all we'd need to control the automatic conversion and how we do it now (with Edit: About the function makeStore(config) {
const store = {};
for (const ref in config) {
store[`_${ref}`] = undefined;
store[ref] = computed(() => {
if (!store[`_${ref}`]) {
loadRef(ref).then(action(refList => {
store[`_${ref}`] = refList;
}));
}
return store[`_${ref}`];
});
}
return observable(store);
} Which is a terrific way of lazily loading stuff, and I can't really use the |
@JabX no in my last proposal you would need: const obj = observable({
prop1: {a: 1, b: 2}
});
observable.props(obj, { prop2: { a: 1, b: 2 } }) Hmm is it confusing that
If you want |
assuming observable.ref is sticky
Op ma 5 dec. 2016 22:10 schreef Damien Frikha <[email protected]>:
… @mweststrate <https://github.com/mweststrate> Could you still do this?
const obj = observable({
prop1: {a: 1, b: 2},
prop2: observable.ref({a: 1, b: 2})
});
I believe this is all we'd need to control the automatic conversion and
how we do it now.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#649 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/ABvGhHy73tvcycs_uCNEvbDLhWJXRVjqks5rFH3PgaJpZM4KwYbt>
.
|
@mweststrate Well, just look at my exemple and yours, and tell me which API is nicer :) What I like to do is building objects from some kind of definitions, and then at the end wrapping it into an Right now, I can do something like that: const myObject = observable({
constants: asReference({a: 1, b: 2, c: 3})
prop1: {
constants: asReference({a1: 1, b1: 2, c1: 3}),
prop11: {foo: 4, bar: 9},
prop12: {baz: 2, bac: 8},
prop13: {x: 9, y: 1, z: 23}
},
prop2: {
// and so on...
}
// and so on...
}) I like this approach and I can't think of a better API to do that. |
yours 😊
but there are several other questions to answer to justify an api:
* type soundness
* does it save work
* level of self explanatoryness
* is it regular with the rest of the api
* size increase of the api surface
* most importantly: how common is the case? is it common enough that it
should have a dedicated function?
Op ma 5 dec. 2016 22:27 schreef Damien Frikha <[email protected]>:
… @mweststrate <https://github.com/mweststrate> Well, just look at my
exemple and yours, and tell me which API is nicer :)
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#649 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/ABvGhJUWWG7b04fRC4ozg_24w1vlgLMnks5rFIHRgaJpZM4KwYbt>
.
|
In a binary conversion-or-non-conversion world, ref simply stops conversion, and you use let data = observable({
coordinates: {lat: 0, lng: 0},
dates: observable.ref(observable.array([]))
}) if you want to stop automatic conversions, but still have an observable array. With the caveat that any reassignment of that property needs to be also wrapped in array() if push/pop/etc need to be observed: data.dates = observable.array(plainArray); My assumption here is that if you are working with refs you want finer control compared to full-automatic, so you explicitly say "an observable reference to an observable array", and explicitly convert regular array values. Automatic one-level-conversion is left out... p.s. with this sort of API using a shorter local name for let data = O({
coordinates: {lat: 0, lng: 0},
dates: O.ref(O.array([]))
})
data.dates = O.array(plainTimestampsArray.map(t => new Date(t))); p.p.s. does anyone have a more convincing use case than my array of date objects? |
I would like to point out that opts is just another name for modifier/descriptor. constructor() {
// So I need a bunch of deeps
observable.props(this, {
prop1: [],
prop2: {}
}, { deep: true, name: "This name doesn't make any sense", extend: thisFnJustALittle });
// And a bunch of references
observable.props(this, {
prop3: [],
prop4: {}
});
// But one deep also needs a name
observable.prop(this, "namedDeep", [], { deep: true, name: "I am deep and need a name"});
// And one reference needs an extension
observable.prop(this, "fancyObservable", {}, { extend: fancyExtension });
// I don't need anything else, because I have commited suicide at this point
} Even if it would be pain to declare each observable prop individually it would be better then this nonsense. Another thing to notice: If I understand right, the
Why options? Why do I need to search what the options are? Why do I need to search what the default options are? Why don't you let me simply create ShallowArray and DeepArray? |
Well it's there already and it works fine. With the changes we would in fact remove all modifiers except for Type soundness isn't an issue since
|
@urugator whats the use case for a shallow array, as opposed to a |
@spion yes interesting use case is storing a reference to a HTML domNode, or JQuery promise object (which is still plain) @urugator @JabX what I try to stress is that convenient and small & simple are each other natural enemies. So I think it is the goal to design something convenient in 80% of the cases, and small and simple to grasp and use in the exceptional cases (but it doesn't have to be convenient per se) So I understand that the proposed api is not in all cases convenient. But how bad is that? (In this case we could even support chaining for What still haunts me, but maybe it is a too generic problem to solve with mobx, is this example from @spion:
So the Instead of @urugator yep,
I think that should be fine as well, especially if the top level api stays small by using
That goes a bit back to the late first proposal, where these functions like const myObject = observable({
constants: with({a: 1, b: 2, c: 3}, modifiers.ref, modifiers.name("floepie"), myCoolLogger)
dates: with([ ], modifiers.shallow)
}) Not sure what a nice syntax / name is here though.
But then again: some options and a lot of repetition in TL;DR:
|
@spion Just to make it clear, I haven't said anywhere that sticky shallows must be supported. Actually I said that sticky shallows should be either removed together with sticky map (deep or not) or the autoconversion must be type safe, otherwise it's not possible to ensure consistency I believe. To your question: Autoconversion(stickyness) is just a conveniency. Anytime you want a setter generated automatically for you, then you want autoconversion. It's just an ability of the API to accept unobservable objects. So anytime you want a publicly settable shallow prop, you probably want autoconversion. It's exactly the same situation as with deep.
|
Primary concern of each program should be to provide a solution to given problem.
There should definitely be a standalone shallow.
Either don't allow defining multiple observable props at the same time, or yes it should be handled differently. (Do you apply single decorator to multiple props at once?, no because it doesn't make sense...) Btw |
Ok, probably we can't escape the modifiers RHS, they are too convenient. So what we get then is:
For comparision what changed to current (MobX2) behavior:
|
Maybe this is my fault for not understanding how MobX works, but I've always assumed that it was the actual behaviour, since it looked too magic to me to autoconvert my array into an observable array with a direct affectation like that.
About your proposal, why would we need both shallow observables and the shallow modifier? @observable arr = observable.shallowArray()
// and
@observable.shallow arr = observable.array()
// and
@observable.shallow arr = [] And if I wanted a ref to a shallow array, I would do @observable.ref = observable.shallow([]) I don't think we even need either version of |
Shallow observables are primarily to create shallow observables in ES5, because |
Yeah I realized that moments after posting. That means we would still need data structures, but only shallow ones then? |
// Autos
deep(initialValue?, name?) // replaces observable(val) (everything is observable, having observable function is misleading, mentioning "observable" is redundant, stating deep instead of observable better indicates the existence of shallow)
shallow(intitialValue, name?) // replaces observable(asFlat(val))
// Standalones
shallowBox(initialValue?, name?) // explicitely states shallow, so there aro no doubts about behavior/defaults and to indicate the existence of deep
shallowArray(initialValue?, name?)
shallowMap(initialValues?, name?)
shallowObject(initialValues, name?)
deepBox(initialValue?, name?) // explicitely states deep, so there aro no doubts about bahivor/defaults and to indicate the existence of shallow
deepArray(initialValue?, name?)
deepMap(initialValues?, name?)
deepObject(initialValues?, name?)
// <- you can place these into mobx.observables namespace if you think it's necessary
// Decorators + Modifiers
@prop // reference
@prop.deep // @observable - autconverts array|object
@prop.shallow // @observable asFlat() - autoconverts array|object
@prop.[type] // sticky, autoconverts to given type: shallow|deep|Box|Array|Map|Object, type safe
// Every decorator accepts name param
@prop.shallowMap("name")
// Without decorators
defineProperties(this, {
prop1: [], // reference
prop2: prop([], "name") // named reference
prop3: prop.deep("name") // named @observable
prop4: prop.shallow,
prop5: prop.[type]
}); EDIT: If there are overloading problems between modifiers and decorators, either force user to always call the decorator/modifiers as function (without name parameter, which makes sense afterall) or move them to separate namespaces (you will never use both at the same time...you either use decorators or not - I would go for mobx.decorators and mobx.descriptors). I personally propose to do both as you will avoid possible problems in the future. |
@mweststrate Just want to give you props on listening to everyone's ideas and coming to a nice conclusion. This was a really long discussion but I think the proposed API makes things even clearer than they are now while not sacrificing any of the "it just works" magic that makes MobX great. |
If the auto conversion always clones, why not also clone own properties of objects with prototypes as well, not just plain objects? It's one less gotcha you have to be aware of. |
@alexhisen because prototyped are considered closed boxes, and can take care of own observability. If they still would be cloned; it means that you always get a copy as soon as you have multiple references to it. |
Closing this issue as [email protected] is now available, which addresses this issue. Changelog: https://github.com/mobxjs/mobx/blob/mobx3/CHANGELOG.md#300. The changes in more details: https://github.com/mobxjs/mobx/pull/725/files. Feel free to re-open if questions arise! |
How does @observable.shallow public array = []; How do I do the same with maps? At the moment I'm doing: @observable.ref public map = observable.shallowMap(); I could probably just do |
@jamiewinder According to changelog:
so I quess the following should work (haven't tested) @observable.shallow public map = new Map(); Without ES6 Map, the sticky shallow map initialized from plain object probably isn't supported, so you would have to do the conversion manually in setter. |
@urugator correct. @jamiewinder if you never replace the map instance (which I consider a good practice), |
Any chance of seeing BurntCaramel's recommendation implemented? |
This is a proposal to kill the automatic conversion of objects / arrays to observables:
1. Stop automatic conversion of observables
While this, on one hand, is very cool and convenient for beginners, because any changes deep inside the todos collection can automatically be tracked. It also is a source of confusion. Although not often a cause of trouble, the mutation-of-the-righ-hand-side-of-an assignment (or
push
in the above example) is weird and hard to track if you didn't expect it. Several people already run into that. See for example #644So after this proposal one would need to do the following:
2. Kill modifiers
Currently, MobX provides an opt out mechanism of this behavior, modifiers like
asFlat
andasReference
.For example modifying the original listing to
todos = observable(asFlat([]))
would have prevented the behavior. So it can be opted-out.Since this proposal removes automagic observable conversion, the need to know about these modifiers is gone and they can be removed. (
computed
,asMap
andasStructure
are still useful modifiers though). This solves a lot of design questions about the most convenient api around modifiers. See for example #211, #197, #563.
observable(object)
should clone instead of enhanceThe above example shows a nice inconsistency between observable objects and arrays (and maps). Objects being converted keep their identity, while arrays produce a fresh object (originally due to limitations in ES5). However to keep things explicit and predictable, I think it is nicer to prevent objects from being enhanced in place, and instead produce fresh objects. In other words, currently
observable(object)
is an alias forextendObservable(object, object)
. With this proposalobservable(object)
would be the same asextendObservable({}, object)
4. Migration path / questions
@observable
decorator is barely effected. Except when assigning objects, arrays. Should those be converted automatically to observables one level deep? E.g. should one need to write@observable todos = observable([])
or should@observable todos = []
suffice (and convert[]
) still automatically? Or should the whole api be more more explicit:@observable todos = observableArray()
?observable
on an already observable thing should probably warnTL; DR
I think this proposal reduces the perceived level of magic introduced by MobX, as the developer is now completely in control when objects become observable and has to be explicit about this. This might reduce the wow-factor for beginners a little bit, but reduce the confusion for intermediate (and beginner) MobX users.
This is definitely a breaking change, so run time guidance on all cases where behavior is changed would be very important I think (unless api method names are also changed, in that case old behavior could live side by side for a while)
The text was updated successfully, but these errors were encountered: