Closed mweststrate closed 4 years ago
@olee I think the first one is neat. For the second one, that is probably only a couple of lines, so I will not will be worth the effort of separating it.
If everything is observable/action/computed by default, then we know which fields must not be made observable, because they must have been marked as ignored in superclass.
@urugator that assumes always using auto observable in the superclass, and it assumes having the superclass under control in the first place. Simple counter example, using makeAutoObservable
on a class MyComponent extends React.Component
could blow up a lot React internals we don't even know about.
But the decorator support could be built tree-shakeable, I guess?
https://blog.logrocket.com/new-decorators-proposal/?amp=1 This part is interesting: "Problems with JIT and decorators"
@mweststrate Idea:
makeObservable
- anything that isn't listed in decorator map is decorated with ignore
. So when you call makeAutoObservable
in subsclass it won't attempt to redecorate already decorated fields.
makeAutoObservable
always throws when called from a subclass of parent that hasn't called
makeObservable/makeAutoObservable
. That is to prevent messing up with "uncontrolled" classes in general.
makeObservable - anything that isn't listed in decorator map is decorated with ignore. So when you call makeAutoObservable in subsclass it won't attempt to redecorate already decorated fields.
This might not work well, because there could be class fields you don't know about.
For example a ts field declaration like public unobservedValue?: number;
in the parent class would not be picked up by this strategy if it does not have any decorator on it.
But for fields that have an initialized value this might be an option.
However, the more I think about this whole solution with makeObservable in general, the more I see it possibly becoming incredible performance heavy.
I think it would be great to have a kinda PoC early to investigate how the performance of such a solution would compare to previous decorators. Could this be done maybe with current's mobx extendObservable
as a test?
makeAutoObservable always throws when called from a subclass of parent that hasn't called makeObservable/makeAutoObservable. That is to prevent messing up with "uncontrolled" classes in general.
This should definitely not happen - it would prevent adding observable fields for subclasses where you do not have control over the parent class.
public unobservedValue?: number;
Hm, if I follow correctly, such field won't be picked even by makeAutoObservable
, so it shouldn't break the parent, but it seems like another problem of it's own.
This should definitely not happen - it would prevent adding observable fields for subclasses where you do not have control over the parent class.
It wouldn't, the point is that you are forced to use makeObservable
(be explicit about what will be observable) instead of makeAutoObservable
. That is to somehow address "No auto decorating for subclassed classes", so it's not breaking things silently.
Well you said
makeAutoObservable always throws when called from a subclass of parent that hasn't called makeObservable/makeAutoObservable
But I guess you rather mean that only makeAutoObservable should throw if the parent class didn't call makeAutoObservable? Because otherwise it is as I said - it would prevent adding observable fields to subclasses where the parent wasn't observable - which is just a different wording of the quote above, ain't it?
class NonObservable {};
class SubclassOfNonObservable extends NonObservable {
constructor() {
super();
makeAutoObservable(this) // throws because parent didn't called makeObservable/makeAutoObservable
makeObservable(this); // never throws - allows adding observable fields
}
}
class Observable {
constructor() {
// Any of
makeAutoObservable(this)
makeObservable(this)
}
}
class SubclassOfObservable extends Observable {
constructor() {
super();
makeAutoObservable(this) // doesn't throw because parent called makeObservable/makeAutoObservable
makeObservable(this); // never throws
}
}
Yeah that makes sense, thanks for the example 😅
@mweststrate Maybe it's fine to use a proxy for this, but the downside is that the browser must support Proxy
. What do you think about it?
class Observable {
constructor() {
return new Proxy(this, {
defineProperty(target: any, key: any, descriptor: any) {
// handle decorator
},
});
}
}
class Foo extends Observable {
@observable
field1 = 'value1';
field2 = 'value2';
constructor() {
super();
}
}
Requiring a base class removes flexibility, for example using decorators in react component classes
Op di 7 apr. 2020 06:45 schreef Michael Lin notifications@github.com:
@mweststrate https://github.com/mweststrate Maybe it's fine to use a proxy for this, but the downside is that the browser must support Proxy. What do you think about it?
class Observable { constructor() { return new Proxy(this, { defineProperty(target: any, key: any, descriptor: any) { // handle decorator }, }); } } class Foo extends Observable { @observable field1 = 'value1';
field2 = 'value2';
constructor() { super(); } }
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx/issues/2325#issuecomment-610186845, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBH3JOHOH3TMXU56E23RLK4V5ANCNFSM4LZQ2B5Q .
Indeed, flexibility is reduced. It can only look like this below, and it looks okay.
class Foo {
@observable
field1 = 'value1';
field2 = 'value2';
constructor() {
makeObservable(this);
}
}
Requiring a base class removes flexibility, for example using decorators in react component classes Op di 7 apr. 2020 06:45 schreef Michael Lin notifications@github.com: … @mweststrate https://github.com/mweststrate Maybe it's fine to use a proxy for this, but the downside is that the browser must support Proxy. What do you think about it? class Observable { constructor() { return new Proxy(this, { defineProperty(target: any, key: any, descriptor: any) { // handle decorator }, }); } } class Foo extends Observable { @observable field1 = 'value1'; field2 = 'value2'; constructor() { super(); } } — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#2325 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBH3JOHOH3TMXU56E23RLK4V5ANCNFSM4LZQ2B5Q .
@mweststrate You wrote on Twitter today that the new mobx will be very good. Which is incredibly good news :) What will it look like from the user's side? Will the new version eat less memory?
Too early to tell but I don't expect any significant memory gains.
Op za 11 apr. 2020 19:51 schreef Grzegorz Szeliga <notifications@github.com
:
@mweststrate https://github.com/mweststrate You wrote on Twitter today that the new mobx will be very good. Which is incredibly good news :) What will it look like from the user's side? Will the new version eat less memory?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx/issues/2325#issuecomment-612489057, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBHI56FCAMTG43ORPB3RMC32XANCNFSM4LZQ2B5Q .
Have you considered an unopinionated MobX core build? Including just your basic TFRP functions (createAtom
, computed
, autorun
/when
/reaction
, onBecomeObserved
, and action
) packaged up in a simple API might make for a lightweight and powerful alternative to RxJS and its friends.
That minimal set of functions should be enough to work with MobX React Lite and would let us sidestep the entire ES5 vs ES6 vs decorators question for a decent number of use-cases.
drop some features like spy, observe, intercept
Oh, no!
I use both intercept
and observe
:
1) I have a store for statistics data with absolute values (count of emails sended, opened etc) and while I put the data to store the hook intercept
calls a function which calculate relative data ( e.g. % opened/sended)
2) Also I have a store Parameters
and there I use observe
for changing url of current page after sone parameters changed
My key message is that stores in Mobx is… umm… smart. It can autorun, lazy compute etc so why it can't avaiblity to manipulate of incoming data? 🤔 Drop observe/intercept will make a store more dumb. That smart black magic is that feature why we really love Mobx. Isn't it?
They won't be dropped :-P. I was just wishful thinking :)
On Thu, Apr 16, 2020 at 4:15 PM Igor «InoY» Zvyagintsev < notifications@github.com> wrote:
drop some features like spy, observe, intercept
Oh, no! I use both intercept and observe:
- I have a store for statistics data with absolute values (count of emails sended, opened etc) and while I put the data to store the hook intercept calls a function which calculate relative data ( e.g. % opened/sended)
- Also I have a store Parameters and there I use observe for changing url of current page after sone parameters changed
My key message is that stores in Mobx is… umm… smart. It can autorun, lazy compute etc so why it can't avaiblity to manipulate of incoming data? 🤔 Drop observe/intercept will make a store more dumb. That smart black magic is that feature why we really love Mobx. Isn't it?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx/issues/2325#issuecomment-614716511, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBFRTLUX2VV7FH6NC63RM4OKJANCNFSM4LZQ2B5Q .
constructor () { makeObservable(this, { field: observable, field2: observable.shallow, method: action, } )
I also like this version more.
Is it possible to set TS to check that all fields that are not readonly must be marked as observable? It would also be nice for TS to check that we marked a field readonly with an observer and treat it as an error.
@szagi3891
Now assuming you mean Typescripts readonly
keyword and/or Readonly<T>
interfaces.
These ideas are bad for two reasons.
readonly
keywordA field being readonly
, doesn't mean its content is immutable, it just means the field itself can't mutate. A use case would be readonly todos: Array<Todo>
.
You can't set a new array on it after the constructor assignment, but you most certainly can mutate the array itself.
Readonly<T>
interfaceThere is a use case where you don't want to allow mutations outside of actions.
You could create private observables like private _todos: Array<Todo>
but then also expose public computed getters. But that can easily be cumbersome if there are so many and has its own issues now[1]
Another approach is Readonly<Array<Todo>>
with this
type Writeable<T> = { -readonly [P in keyof T]: T[P]};
class Store {
readonly todos: Readonly<Array<Todo>>
//assume set up in constructor
addTodo = (todo: Todo) => {
const list: Writeable<Array<Todo>> = this.todos
return list.push(readyTodo)
}
}
So I hope that readonly
& Readonly<T>
are not disabled from being observables.
Though I would welcome a more succinct and non-intrusive approach for making private mutable observables
& public immutable computed
for a class that works out of the box.
[1]: My current issue is that there is no way to assign a decorator on private observables
class Store {
private _todos: Array<Todo>
//constructor setups
}
decorate(Store, {
_todos: observable
})
Edit: Added solutions and workarounds for the issues brought up: https://github.com/mobxjs/mobx/issues/2340#issuecomment-619160940
I know this issue is for MobX, but I'm interested in the topic of dropping decorators in general (until a proposal actually has success), and this issue happens to be one of the top results on Google when searching for terms relating to moving away from decorators and TypeScript.
@mweststrate Regarding the example with the method,
increment = action(function () {
this.value++
})
and then in the constructor initializing the decoration: the [[HomeObject]]
of the method will not be set, and we will not be able to access super
unfortunately, or if we use arrow functions super
will not work as we'd like in subclasses (we won't be able to extend methods). This is a hindrance for method use in the general sense.
Would perhaps the decoration step in the constructor also store the method on the prototype (only the first time) just to make it possible for a subclass to do something like
incrementMore = action(function () {
SuperClass.prototype.increment.call(this)
this.value++
})
as an alternative to using super
?
I haven't used MobX before; maybe actions in MobX don't need to call super.method()
s anyways, but I'm still curious about methods in general for any lib moving away from decorators.
One problem with the decorative values (for lack of a term of the above pattern) is with Custom Elements and decorating fields that should map to/from DOM attributes.
Decorators allow us to do things like modify static observedAttributes
based on decorated properties before construction time. With the decorative value pattern (setting class fields with special values then calling initializeObservables(this)
in class constructors), the decoration happens too late: customElements.define
calls read values from observedAttributes
immediately, before any instance of a passed-in class is ever created, therefore if initializeObservables(this)
adds items into the observedAttributes
array after the element is already defined those attributes will never be observed; we can not instantiate a custom element class before calling customElements.define
(it will throw).
For the Custom Element case, we have to use the older pattern that MobX already uses, f.e. calling a function after the class definition:
class MyEl extends WebComponent(HTMLElement) {
foo = 'bar'
bar = 'baz'
}
mapAttributesToProps(MyEl, ['foo', 'bar'])
// ^ The WebComponent base class may utilize meta data in attributeChangedCallback for example
Another way could be to rely on pre-defined observedAttributes
values:
class MyEl extends WebComponent() {
static observedAttributes = ['foo', 'bar']
foo = 'bar'
bar = 'baz'
constructor() {
super()
mapAttributesToProps(this)
}
}
or use the explicit decorative values to avoid clobbering super
properties that weren't meant to be mapped:
class MyEl extends WebComponent() {
static observedAttributes = ['foo', 'bar']
foo = attribute('bar')
bar = attribute('baz')
constructor() {
super()
initialize(this)
}
}
Either way there's some unwanted duplication.
One of the best things about decorators is easily avoiding duplication while also being more terse.
With decorative values, how do we apply them to accessors (and not ruin super
calls)? The nice thing about accessors and legacy decorators, is that setters can be the handlers for incoming attribute strings:
class MyEl extends WebComponent() {
this._foo = 0
this._bar = 0.0
@attribute
set foo(val) { this._foo = parseInt(val) }
get foo() { return this._foo }
@attribute
set bar(val) { this._bar = parseFloat(val) }
get bar() { return this._bar }
constructor() {
super()
initialize(this)
}
}
How would we use decorative values with get/set? Maybe we can have an accessor backed by a value-decorated field:
this._foo = attribute(0)
set foo(val) { this._foo = parseInt(val) }
get foo() { this._foo }
constructor() { super(); initialize(this) }
but that has various issues (like how the decoration would map foo
instead of _foo
(possible but easy to break), plus now _foo
is probably a second accessor layer).
Is there another way?
Your "property read/write trapping decorators" proposal would make this terse and clean, eliminating the cache variables and eliminating duplication.
Something I've seen libraries do and that I'd like to avoid, is accept handlers in decorators, which I feel makes things harder to understand and read (especially when stepping over code in devtools due to indirection in the code paths):
@attribute((value) => {
// ... handle the input value ...
}) foo = 0
(which only works in Babel with lose fields, or for now TypeScript by default but soon not, and hence the whole reason to avoid decorators which will save people headaches and confusion).
The equivalent of the last example with decorative values would be:
foo = attribute((value) => {
// ... handle the input value ...
}, 0)
which is still somewhat awkward, but perhaps a little better considering that foo =
now comes in front, so the intent of creating a property is still clear up front. Maybe we can swap the arg order to make it better:
foo = attribute(0, (value) => {
// ... handle the input value ...
})
This is reasonable. It is almost as if we're creating or instantiating an attribute.
The bottom line is decorators will do wonders to code clarity and terseness. I can't wait to use them. But for now, I agree it is better to stick to patterns that will work everywhere (Babel, TypeScript, or vanilla JS, etc) without any doubts, unlike the current decorators that might work perfectly in TypeScript only to fail utterly when a Babel user imports them (which is a nasty problem for any library to have). These problems cause time waste and headaches that library authors and end users don't want to have.
If only class fields didn't land with [[Define]] semantics, then keeping legacy decorators might at least be ok because they'd work consistently across environments...
Is there any chance implementors of major JS engines would ever consider moving class fields to [[Set]] semantics? We broke the web. Can we break it one more time to fix it?
@trusktr although I agree with the general sentiment, this is not the repository to discuss the future of JavaScript. That is what, in this case, https://github.com/tc39/proposal-decorators is for. So I'll gonna mark the post as off-topic
Another strong vote against removing decorators.
But if it comes to this, I think having mobx-decorators
the same way we have mobx-react
would be a reasonable tradeoff.
https://github.com/mobxjs/mobx/issues/2325
@mweststrate
The downside of this approach is that I don't know how to do "auto" decorating with some exceptions. An additional API like makeAutoObservable(this, perMemberMap?) looks a bit ugly, although it is not too bad
I found it ugly too. Especially as there may be different ways of auto: In a class of mine all getters are computed, all fields but one are observable, but only about a half of functions are actions, others are views (in the MST sense). So I'd use {field: observable, getter: computed, function: error}
as the default, which would force me to enumerate all functions, but probably save me some problems (because of function: error
).
So I guess, the auto feature should be configurable, maybe like makeObservable(this, perMemberMap?, perMemberTypeMap?)
with perMemberMap
mapping members (field1, function2, ...) and perMemberTypeMap
mapping member types ("field", "getter" and "function").
Moreover, there probably should be a global defaultPerMemberTypeMap
in mobx, with the effective value given by {...defaultPerMemberTypeMap, ...perMemberTypeMap}
. This would make clear how exactly "auto" works.
I would leave the makeAutoObservable
to userland.
The idea was not to safe keystrokes, but to provide safety (so you cannot forget observable/action etc).
However it presumed that there is only opt-out behavior and that we can reliably itrospect the class, which doesn't seem to be the case (prive fields/optional fields/subclasses...?).
It shouldn't be hard to come up with own implementation that suits the user's needs, while being aware of potential limitations.
I've been thinking whether the view/action
problem isn't a little bit artificial:
The action is mainly intended to provide batching. View with batching is fine, but we cannot use action for views because it also applies untracked.
We also cannot use batch
alone, because "strict mode" requires action
(even though it's still mainly about batching).
The only reason why action
applies untracked is so it can be "safely" invoked from derivation (computed/autorun) ... however synchronously mutating state from derivation is most of the time antipattern or edge case.
So potentially we could remove untracked from action
(1), so it can be applied automatically to every function and forbid state mutations from derivations unless you explicitely allow that by wrapping it with effect
(which applies untracked
).
[1] Semantically it doesn't make sense to apply action
to view
, what I actually mean is to provide something that doesn't apply untracked
, while still satisfying the strict mode, therefore can be automatically applied to any function. It doesn't have to be called action
and it doesn't mean that action
has to go away...
Actually I think it could be simplified like this:
Everything stays as is.
The only difference is that makeAutoObservable
will use the following function instead of action
:
function wrap(fn) {
return (...args) => {
// provide batching
startBatch()
// allow reads
allowStateReadsStart()
// !!! untracked is missing so it works with views
// !!! allow state changes only when outside of derivation
if (notInsideDerivation()) {
allowStateChangesStart()
}
const result = fn(...args)
if (notInsideDerivation()) {
allowStateChangesEnd()
}
allowStateReadsEnd()
endBatch()
return result;
}
}
As a consequence if the auto decorated function mutates state, it will throw when called from derivation (computed/autorun), so you will be forced to wrap it in runInAction/action
manually - either at definition:
makeAutoObservable({
method: action, // now you can call it even from derivation
})
or inside derivation:
autorun(() => {
runInAction(() => store.anAutoDecoratedMethod())
})
@urugator yes, I start to see the light! I think you are spot on. Looking back,
untracked
part is to make sure actions called from autorun aren't tracked, allowing you to split of the observation and effect part. However, reaction
solves that problem now in a more elegant wayI think we can indeed introduce what you are proposing, let me know if this summarizes it correctly:
MobX has a global context state (or stack) that is either NONE
, UPDATING
or DERIVING
method | base state | new state |
---|---|---|
track* | NONE | DERIVING |
track | UPDATING | DERIVING |
action | NONE | UPDATING |
action | DERIVING | DERIVING |
runInAction / allowStateChanges | * | UPDATING |
untracked | * | NONE |
* track is the underlying primitive of autorun / computed
computed
and autorun
e.a. bring the state to DERIVING
action
brings the state to UPDATING
, but only if the current state is NONE
runInAction
and allowStateUpdates(true)
brings the state to UPDATING
regardless the current state, as escape hatch for lazy-cache-updating scenarios. We might be able to deprecate allowStateUpdates
, but IIRC the false
case also exists, so would have to investigateuntracked
brings the state back to NONE
(so an action call inside a reaction inside untracked for example will allow state updates again)DERIVING
stateenforceAction
keeps working as is, warning when updates happen in NONE
stateallowStateChangesInsideComputed
can probably be deprecated? I have to search back what that one was good for again
Well basically that goes back to the initial idea - it all sort of depends on how much we are willing to change API, introduce BCs and how far we want to go with these checks.
I think you got it mostly right, but:
Semantically it's a bit weird that the action
is usable as a view (when it doesn't mutate state).
Throw on write detection to enforce correct context isn't bullet proof:
computed(() => {
// this throws so the user is forced to use `runInAction` here
myAction();
})
const myAction = action(() => {
// but actually it throws here, so we can fix it here
runInAction(() => {
observable.set(5);
});
})
Potentially we could restrict from which to which context the transition can occur, but then we may need to again differentiate between view/action
(not sure atm).
runInAction
shouldn't have a different meaning than action
and allowStateChanges
is just some low level thing which by itself doesn't map to any real use case, that's why I introduced that effect
function, so it has semantic meaning (also it's analogous to reaction
's second arg).
untracked
imo shouldn't have any effect - it shouldn't (dis)allow reads/writes. It should simply disable subscriptions without changing the context. Again a low level thing that doesn't map to any real use case.
There is obserevableRequiresReaction
, which basically says: Throw if you read something outside of DERIVING
or UPDATING
- which also means that you wouldn't be able to perform reads inside untracked
(if it would change the context to NONE
), which is mental, because untracked
is used for reads in the first place.
EDIT: One more concern... Is it possible for useEffect
to be ever invoked immediately during render (inside derivation)?
@mweststrate Sorry, I didn't mean to hijack the issue. I was showing my custom element example hoping to discuss similar issues that would happen in MobX.
Let me ask smaller MobX-specific questions one at a time:
If MobX rolls with the approach of
class Doubler {
value = observable(1)
double = computed(function () {
return this.value * 2
})
increment = action(function () {
this.value++
})
constructor() { initializeObservables(this)}
}
then we want to extend it,
class DoubleIncrementer extends Doubler {
increment = action(function () {
super.increment() // ?
super.increment() // ?
})
}
how would we do that considering that super
wouldn't work there?
In https://github.com/mobxjs/mobx/issues/1864, you said
Don't bind methods in super classes, it doesn't mean in javascript what you hope it means :-)
If MobX gives people a class
-based tool, they may expect to be able to use class
features like they do in plain JS without MobX.
MobX 6 won't go with that proposal, but the second one
On Mon, May 4, 2020 at 2:47 AM Joe Pea notifications@github.com wrote:
@mweststrate https://github.com/mweststrate Sorry, I didn't mean to hijack the issue. I was showing my custom element example hoping to discuss similar issues that would happen in MobX.
Let me ask smaller MobX-specific questions one at a time:
If MobX rolls with the approach of
class Doubler { value = observable(1) double = computed(function () { return this.value * 2 }) increment = action(function () { this.value++ }) constructor() { initializeObservables(this)} }
then we want to extend it,
class DoubleIncrementer extends Doubler { increment = action(function () { super.increment() // ? super.increment() // ? }) }
how would we do that considering that super wouldn't work there?
In #1864 https://github.com/mobxjs/mobx/issues/1864, you said
Don't bind methods in super classes, it doesn't mean in javascript what you hope it means :-)
But if MobX gives people a class-based tool, they expect to be able to use class features.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx/issues/2325#issuecomment-623224126, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBHML3MRNXDQZHSILX3RPYNBZANCNFSM4LZQ2B5Q .
The explicit one like initializeObservables(this, { foo: observable, bar: computed, etc })
?
Yes
On Mon, 4 May 2020, 07:03 Joe Pea, notifications@github.com wrote:
The explicit one like initializeObservables(this, { foo: observable, bar: computed, etc })?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx/issues/2325#issuecomment-623274109, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBDJANONQFSOPHE3Z3LRPZLEVANCNFSM4LZQ2B5Q .
I suppose private class fields are out of the question, as there is no way to access those dynamically from initializeObservables(this, ...)
. They're locked out until decorators finally do land, it seems.
Correct
Op zo 10 mei 2020 19:03 schreef Joe Pea notifications@github.com:
I suppose private class fields are out of the question, as there is no way to access those dynamically from initializeObservables(this, ...)
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx/issues/2325#issuecomment-626365889, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBF4G6AZKWOGKN4MOQ3RQ3T5JANCNFSM4LZQ2B5Q .
The last comment was about JS privates, not TS privates
On Mon, 11 May 2020, 01:08 Seivan, notifications@github.com wrote:
@mweststrate https://github.com/mweststrate What happened with this idea https://github.com/mobxjs/mobx/issues/2339#issuecomment-619376221 to tackle that?
@trusktr https://github.com/trusktr
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx/issues/2325#issuecomment-626411131, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBD6I5ARFPRIXUU2YVTRQ46WBANCNFSM4LZQ2B5Q .
I think the example with value: computed(function () { ... })
is much better than the initializeObservables
approach - the latter reminds me of MobX-without-decorators, which led to a lot of debugging because I would forget to mark certain fields as observable/action/computed. The "auto initialize" concept is interesting (similar to autobind
) but there are cases where I don't care about a field being an observable, and if it's bad for memory/CPU then I'm fine without it.
reminds me of MobX-without-decorators
Yep, but in the constructor to overcome [[Define]] semantics of class fields.
Maybe that's best: it remains familiar and without performance cost of "auto initialize" (though an auto can be convenient in cases where the perf hit doesn't matter).
To clarify the original post didn't reflect anymore the new api of V6, instead we opted to implement the alternative api as found in the original post as discussed earlier in this thread. I just updated the post to better reflect the new api. So we won't be using value wrappers, but rely on makeObservable(this, map)
instead.
I love @robbiewxyz idea, lets simplify API, I would like to see better models API
I'm trying to understand the behavior of makeObservable
and makeAutoObservable
correctly so I can document them properly.
From the code I see that if you call makeObservable
without arguments and no decorator members are found, it errors out. I think that's good.
I imagine this may be difficult to implement, but it would be nice if we could get some kind of warning if you forget to call makeObservable
and use decorators.
What should happen if you call makeObservable
with an annotations argument but there are also decorators? Should it complain?
Concerning makeAutoObservable
, I see its exceptions argument is an annotation map. How do you then exclude a particular key from being made observable? I think that's done with 'false' if I read the type correctly.
What should happen if you use makeAutoObservable
on a class that uses decorators?
I think it's interesting to consider that since the names and signatures of makeObservable
and makeAutoObservable
are quite similar what happens if someone mixes them up by accident. What are the scenarios where this would really confuse developers, and are there ways we can help?
Some comments about the 'undecorate' codemod. This turns decorator-based code into code that doesn't use decorators, and also converts code that uses 'decorate'.
Since MobX still supports decorators with makeObservable
, should there be a codemod that only adds makeObservable(this)
everywhere? [edit: I see that there is a keepDecorators
option to the codemod, though no tests for it yet]
Should there a feature that uses makeAutoObservable
when it detects this is possible?
Ok, pushed some changes to v6 branch, I think there is now a decent solution to the is-a-function-an-action-or-derivation dance. I introduced a separate concept of autoAction
, that is to be used primarily by library functions, not directly by users, that is used by makeAutoObservable
, observable(object)
and the lite hooks. autoActions
will on the fly choose whether they act as action (if not deriving anything already) or derivation (when inside another derivation). They batch in both cases, does only untracked in the first case, and only allows state changes in the first case as well.
If the user does updates in an autoAction
that is called from a derivation, it will warn that it must be wrapped in runInAction
to clarify the intend. I also updated that the predicates of when
and reaction
don't allow state changes by default, just like computed
, for consistency.
@faassen
I imagine this may be difficult to implement, but it would be nice if we could get some kind of warning if you forget to call makeObservable and use decorators.
Would be awesome, but no idea how to go about that :) We could maybe at random places (e.g in observableValue) check in DEV mode if a value is a class instance with decorators, but with undecorated members. Might be a bit of a performance bummer though.
What should happen if you call makeObservable with an annotations argument but there are also decorators? Should it complain?
I think it should be ok? not sure :)
Concerning makeAutoObservable, I see its exceptions argument is an annotation map. How do you then exclude a particular key from being made observable? I think that's done with 'false' if I read the type correctly.
Correct
What should happen if you use makeAutoObservable on a class that uses decorators?
Decorate remaining members? or die? Not sure :)
I think it's interesting to consider that since the names and signatures of makeObservable and makeAutoObservable are quite similar what happens if someone mixes them up by accident. What are the scenarios where this would really confuse developers, and are there ways we can help?
No idea :)
Since MobX still supports decorators with makeObservable, should there be a codemod that only adds makeObservable(this) everywhere? [edit: I see that there is a keepDecorators option to the codemod, though no tests for it yet]
Correct. Feel free to add 😅
Should there a feature that uses makeAutoObservable when it detects this is possible?
No I don't think so. MobX is already considered to be quite magically so I think simple > short. Especially if the code is generated anyway 😄 . I think we can apply the same simple > short principle to the docs, thinking about it.
P.s. for detailed semantic questions, I recommend to ask them on the v6 PR rather than in this thread, as it might spam a lot of subscribers :)
Something that I imagine needs to be in mobx-react is a codemod that converts the observer
from a decorator into a function call. I mention it here as I don't see it in notes.md yet.
Something that I imagine needs to be in mobx-react is a codemod that converts the
observer
from a decorator into a function call. I mention it here as I don't see it in notes.md yet.
Do you mean for class components? I don't think it necessary, at least not at the moment. There are no technical difficulties with supporting @observer
. In fact with class components, it feels like an easier way. Chances are that people will eventually migrate toward functional components so it's probably premature.
Just a personal observation, but i too want to use mobx without decorators. I have refactored 500-1k line class stores to use non-decorator syntax, easy peasy. But have you considered the cost of maintaining the non-decorated models compared to the inline decorator syntax? Its 100% impossible for humans to mentally keep track of 2 objects that large , so you have to comment above each class member as //action, obs, obs.ref, computed etc. So if you’re going to do that, why not just inline decorate? jsx / vue / angular / svelte etc arent es spec either , so does being es spec compliant really matter when you have to transpile anyway? I too strongly want to run mobx code without transpiling first, but when i self reflect , i cant help but think its just something i have to deal with because the alternatives are worse
Since we're changing defaults anyway, should the computedRequiresAction be the default in MobX 6? Or perhaps in the spirit of "there should be only one way to do it" remove it entirely as the docs say (somewhat unclear to me): "Though this restriction is confusing and contradictory Computeds can be altered to work in a direct access manner with some of the following methods..."
I wouldn't remove computedRequiresAction
entirely cause some people like it. I definitely support making it the default though!
I agree with what seems like the majority sentiment here - to keep decorators. There hasn't been any stated legitimate reason to remove them honestly. This whole "conundrum" just seems odd to me.
@johnhamm Have you actually read Goals in the first comment? There is a bunch of legitimate reasons for such a decision.
Also, afaik decorator support will not be removed. Instead it's just that decorators are not the primary implementation method and instead in the new implementation they are just providing metadata which the make-observable function can use to know how to handle which field.
@mweststrate does the new v6 branch already contain decorator support? Or is it for now code-configuration only? If not, did you already decide whether decorator support will be a separate package & when it will be added?
MobX 6
Hi folks, I've tinkered a lot about MobX 6 lately, so I want to layout the vision I have currently
Goals
🧨 1. Become compatible with modern ES standards
Let's start with the elephant in the room. I think we have to drop the support for decorators. Some have been advocating this for years, others totally love decorators. Personally I hate to let decorators go. I think their DX and conciseness is still unparalleled. Personally, I am still actively engaged with TC-39 to still make decorators happen, but we are kinda back to square one, and new proposal will deviate (again) from the implementation we already have.
Dropping decorators has a few advantages, in order of importance (imho)
[[define]]
over[[set]]
semantics, all our decorator implementations (and thedecorate
utility) are immediately incompatible with code that is compiled according the standard. (Something TypeScript doesn't do, yet, by default, as for TS this is a breaking change as well, unrelated to decorators). See #2288 for more backgrounddecorate
). I expect to drop a few KB by simply removing them.The good news is: Migrating a code base away from decorators is easy; the current test suite of MobX itself has been converted for 99% by a codemod, without changing any semantics (TODO: well, that has to be proven once the new API is finalized, but that is where I expect to end up). The codemod itself is pretty robust already!
P.s. a quick Twitter poll shows that 2/3 would love to see a decorator free MobX (400+ votes)
😱 2. Support proxy and non-proxy in the same version
I'd love to have MobX 6 ship with both Proxy based and ES5 (for backward compatibility) implementations. I'm not entirely sure why we didn't combine that anymore in the past, but I think it should be possible to support both cases in the same codebase. In Immer we've done that as well, and I'm very happy with that setup. By forcing to opt-in on backward compatibility, we make sure that we don't increase the bundle size for those that don't need it.
P.S. I still might find out why the above didn't work in the past in the near future :-P. But I'm positive, as our combined repo setup makes this easier than it was in the past, and I think it enables some cool features as well, such as detection of edge cases.
For example we can warn in dev mode that people try to dynamically add properties to an object, and tell them that such patterns won't work in ES5 if they have opted-in into ES5 support.
💪 3. Smaller bundle
By dropping decorators, and making sure that tree-shaking can optimize the MobX bundle, and mangling our source aggressively, I think we can achieve a big gain in bundle size. With Immer we were able to halve the bundle size, and I hope to achieve the same here.
To further decrease the build, I'd personally love to drop some features like
spy
,observe
,intercept
, etc. And probably a lot of our low-level hooks can be set up better as well, as proposed by @urugator.But I think that is a bridge too far as many already rely on these features (including Mobx-state-tree). Anyway I think it is good to avoid any further API changes beyond what is being changed in this proposal already. Which is more than enough for one major :). Beyond that, if goal 2) is achieved, it will be much easier to crank out new majors in the future :). That being said, If @urugator's proposal does fit nicely in the APIs proposed below, it might be a good idea to incorporate it.
4. 🛂Enable strict mode by default
The 'observed' one, that is.
🍿API changes
UPDATE 22-5-20: this issue so far reflected the old proposal where all fields are wrapped in instance values, that one hasn't become the solution for reasons explained in the comments below
This is a rough overview of the new api, details can be found in the branch.
To replace decorators, one will now need to 'decorate' in the constructor. Decorators can still be used, but they need to be opted into, and the documentation will default to the non-decorator version. Even when decorators are used, a constructor call to
makeObservable(this)
is needed, the type will be picked from the decoratorsmakeAutoObservable(this, exceptions?)
that will default to observable for fields, computed for getters, action for functionsProcess
Timeline
Whatever. Isolation makes it easier to set time apart. But from time to time also makes it less interesting to work on these things as more relevant things are happening in the world
CC: @fredyc @urugator @spion @Bnaya @xaviergonz