Closed Kukkimonsuta closed 4 years ago
@xaviergonz yeah, it is more babel I am worried about :) iirc babel-decorators-legacy is not compatible with modern field initializers, but above mentioned plugin (hopefully) is. So my goal is to have a simple decorator implementation that is compatible with babel, TS and define semantics. But which combination actually works has to be investigated still. Just want to figure out the general direction first
I only used Reflect.defineMetadata
out of habit! You could use any old Map
/ WeakMap
to remember that metadata if we don't want to depend on that compiler mode or the reflect-metadata module :grinning:
Either way, I don't think the emitDecoratorMetadata mode is required. Using the reflect-metadata module is less invasive, but also optional - although it does take care of poly filling Map/Set as appropriate.
@mweststrate Am I correct that your sixth example will not work because primitives should be wrapped in observable.box
? https://github.com/mobxjs/mobx/issues/2288#issuecomment-593008340
This is how your example will look like with observable.box
:
class Todo {
width = observable.box(20);
height = observable.box(10);
surface = computed(() => {
this.height.get() * this.width.get() * this.utility();
})
double = action(() => {
this.width.set(this.width.get() * 2);
})
utility() {
return 1;
}
}
This example is much more verbose. Or there are plans to get rid of observable.box
?
the idea is that initializeobservables(this)
could automatically unbox
the observable boxes (observable objects already use an observable box per
property behind the scenes). So techincally it would be pretty straight
forward to have it behave the same: width = observable(20)
creates an
observable.box, then initializeObservables generates a getter and setter
for width that redirect to the box's getter and setter.
On Wed, Mar 4, 2020 at 12:21 PM Egor Gorbachev notifications@github.com wrote:
@mweststrate https://github.com/mweststrate Am I correct that your sixth example will not work because primitives should be wrapped in observable.box? #2288 (comment) https://github.com/mobxjs/mobx/issues/2288#issuecomment-593008340 This is how your example will look like with observable.box:
class Todo { width = observable.box(20); height = observable.box(10);
constructor() { initializeObservables(this) }
surface = computed(() => { this.height.get() this.width.get() this.utility(); })
double = action(() => { this.width.set(this.width.get() * 2); })
utility() { return 1; } }
Or there are plans to get rid of observable.box?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx/issues/2288?email_source=notifications&email_token=AAN4NBGWOADMCRQZFAVAG33RFZBUVA5CNFSM4KV4PIV2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOENXS4QY#issuecomment-594488899, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBC7ANMFVJ7KOLP4JQ3RFZBUVANCNFSM4KV4PIVQ .
So I am still limping on two different lines of thoughts here,
.get()
has to be called explicitly like in @kubk example. This is nice for purist caring about side-effectfull property reads / writes, but the main benefit is that it reduces the many ways of achieving the same thing in MobX, making learning easier, where 'unboxing' is just an incremental convenience step, rather than a totally different way of writing things, like extendObservable
vs. decorators vs. decorate. Edit: Some further realizations: option 2 doesn't require any transpilation at all in modern JS. option 1 shares methods by default on the prototype (pro!) but 2 binds them by default (a different pro) (non-bound shared methods is still possible, by doing e.g. Class.prototype.method = action(Class.prototype.method)
after class definition)
Edit: hypothesis: initializers that refer each other need to make sure to read 'boxed'. (but at least that would result in tracked reads in contrast to when using decorators, which might be even more confusing. Anyway, lets make sure we cover that in tests in any case)
Edit: created a poll because the collective twitter wisdom can always be trusted: https://twitter.com/mweststrate/status/1235227165072822272
For reference, this is how Ember does it (since basically two month, I guess we have been some inspiration :)):
class Person { @tracked firstName; @tracked lastName; @tracked age; @tracked country; constructor({ firstName, lastName, age, country }) { this.firstName = firstName; this.lastName = lastName; this.age = age; this.country = country; }
class Counter {
@observable declare count: number;
constructor({ count }: Counter) {
this.count = count;
}
}
The declare
keyword is not available in codesandbox, but it is available in the actual typescript compiler. Of course, if you use the declare
keyword, it becomes more verbose and can't use field initializers, but giving the initial value in the constructor can keep the code clean even when initializing the store for testing or SSR. What do you think?
reduces the many ways of achieving the same thing in MobX
I dunno... do we create observable objects like this:
const o = {
width: observable.box(20),
height: observable.box(10),
surface: computed(() => {
this.height * this.width * this.utility();
}),
double = action(() => {
this.width = this.width * 2;
}),
};
initializeObservables(o);
How do I create an actual box?
class {
width = box(box(20));
computed = computed(computed(() => {}));
constructor() {
initializeObservables(this);
}
}
How do I create a ref to box?
class {
width = ignoreMe(box(20));
constructor() {
initializeObservables(this);
}
}
What about other "boxes": array/map/set (or other classes?)
class {
map: observable.map(); // is this.map observable?
map: observable.box(observable.map()); // or do i need "extra" box
constructor() {
initializeObservables(this);
}
}
If we want to simplify things I still vote for:
// Creating object:
const o = observable(object, decorators)
// Extending object
o.newProp1 = "a";
extendObservable(object, decorators)
// Extending instance
class {
constructor() {
extendObservable(this, decorators)
}
}
// In all cases the default behavior is same:
// fn => action (bound)
// get => computed
// other => observable
extendObservable
can additionally look for decorators on prototype if one prefers an actual @decorator
or decorate
Have to go atm, so quick reply without looking into individual cases, but
we'd always box, which would be basically no different from now where
@observable x = {}
, just like x = observable({})
would both create an
observable object, and an observable property that holds that object
On Wed, Mar 4, 2020 at 6:25 PM urugator notifications@github.com wrote:
reduces the many ways of achieving the same thing in MobX
I dunno... do we create observable objects like this:
const o = { width: observable.box(20), height: observable.box(10), surface: computed(() => { this.height this.width this.utility(); }), double = action(() => { this.width = this.width * 2; }), };initializeObservables(o);
How do I create an actual box? Like this?
class { width = box(box(20)); computed = computed(computed(() => {})); constructor() { initializeObservables(this); } }
How do I create a ref to box?
class { width = ignoreMe(box(20)); constructor() { initializeObservables(this); } }
What about other "boxes": array/map/set
class { map: observable.map(); // is this.map observable? map: observable.box(observable.map()); // or do i need "extra" box constructor() { initializeObservables(this); } }
If we want to simplify things I still vote for:
// Creating object:const o = observable(object, decorators) // Extending objecto.newProp1 = "a";extendObservable(object, decorators) // "Extending" classclass { constructor() { extendObservable(this, decorators) }
}// In all cases the behavior is same:// fn => action// get => computed// other => observableextendObservable can additionally look for decorators on prototype if one prefers an actual @decorators or decorate
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx/issues/2288?email_source=notifications&email_token=AAN4NBBMRZND4QHYGGABKOTRF2MJ5A5CNFSM4KV4PIV2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOENZNNEQ#issuecomment-594728594, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBD4FGKHHHKE67L27ZLRF2MJ5ANCNFSM4KV4PIVQ .
@mweststrate Sounds like a show stopper for CRA based apps that cannot (officially) configure Babel plugins. Feels like extra hurdle people might be reluctant to accept.
What about using Babel macros? I have no idea whether the necessary transformations are possible in macros, but it is worth checking.
@urugator care to share the babel transform?
I dunno... do we create observable objects like this:
No, we can keep that as is / support both. The main goal is to be able to convert classes while keeping 'decorators' co-located
How do I create an actual box? How do I create a ref to box?
I don't think there any actual use case for that, but if there is, an assignment in the constructor or double wrapping would work in deed
map: observable.map(); // is this.map observable?
yes, like with @observable
, you will get automatically a box for the property that holds the ref to the observable collection
@wickedev as far as I can see that still won't work without another abstraction that interprets the decorators; with define semantics the local fields still shadow the decorators by default playground
@urugator
Eg. calling action from computed or reaction is detectable and both can throw.
It is an interesting thought, but I feel like we have been kept piling rules and ways to make exceptions to rules on top of each other (e.g. calling actions from reaction is quite valid, and lazy loading patterns where a computed might trick a side effect action if called for the first time, like in mobx-utils, or making props observable or stores mobx-react(-lite) observable has resulted in a lot of _unblessed
api's, which are ok for building libs, but which we don't want to encourage to be used, as it is too easy too overuse them without grokking the mental model of mobx fully.
In hindsight I'd love to rely on naming convention, e.g. only actX...
for auto conersion, but that would create impossible migration paths at this point :) I don't think this proposal in its current shape removes the need for those _unblessed api's at this moment, but at least hopefully prevents in making the hole deeper.
I think in the end if we don't have enough info to always make the correct choice, opt-in is better than opt-out. That being said, feel free to flesh out a bit how that api would look like?
In short, I'd be interested how we can design a class that has members that should not be observables, and methods that should not be actions. I think in any case it would be nice to add an initializeObservables(this, 'auto')
for classes that don't need exceptions.
Also, better name for initializeObservables
is welcome. Was thinking about unbox
, or mobxInit
, but that might be too abstract :)
flesh out a bit how that api would look like?
observable
(fn + decorator)
Like now, applied by default to fields.
computed
(fn + decorator)
Like now, applied by default to getters.
action
(fn + decorator)
Applied by default to functions.
Cannot be called from derivations (computed/reaction(first arg)/autorun) (unless inside effect
).
Throws: Calling action from computed/reaction/observer is forbidden. If this is an utility function that doesn't mutate state please use ingore
. Mutating state during derivation can lead to inifinite cycles, can break batching and can lead to unexpected behavior. Consider refactoring to computed
or use effect
, see docs, blah blah.
effect
(fn + decorator)
Like action, but can be called from derivations, allows mutating observables inside computed/reaction/autorun.
ignore
(decorator)
For utility methods and fields that shouldn't be observable for whatever reason.
Also makes sure that if you (repeatedly) call extendObservable
on already observable object it won't attempt to make the field observable.
Additionally:
Mutating state from anywhere outside of action/effect
is not allowed.
Reading observables from anywhere outside of reaction/action/effect
is not allowed.
Therefore:
We don't need specific view
decorator (ignore
by itself doesn't allow mutations).
We can ditch enforceAction/requireReaction/requireObservable/etc, these are always on and cover your back.
We can ditch _allowSomething as they can be replaced by effect
(maybe not always not sure..).
care to share the babel transform?
Sure, but keep in mind it's the first thing I tried with babel and it's very unfinished. Personally I don't need it for anything, just wanted to try something new: https://gist.github.com/urugator/ffc92893fafd70d6e52019f38594ece9
If the decorator
argument was a function (or visitor) it would allow default policies to be decided per project. Could even implement things like naming convention based policies. And would allow such things to be factored out into independent npm packages.
Edit: To clarify
import customPolicy from './mobx-policy';
const customInitializeObservables = (self, options) => initializeObservables(self, customPolicy, options);
//...
customInitializeObservables(this,{...})
I think in any case it would be nice to add an
initializeObservables(this, 'auto')
for classes that don't need exceptions.
And maybe something like initializeObservables(this, 'auto', {someField: 'ignore', someAction: 'action'})
when there are exceptions.
Using initializeObservables(this, 'auto', {ignore: ['someField', 'someField1'], action: ['someAction', 'someAction2']})
might be better (more compact, but possibly more error-prone).
I'm unsure what's more common, actions or views. Maybe there should be auto-action
and auto-view
instead.
For naming conventions, initializeObservables(this, 'naming')
could be used; possibly with added exceptions as above.
Also, better name for
initializeObservables
is welcome. Was thinking aboutunbox
, ormobxInit
, but that might be too abstract :)
As it's about the only user-facing thing mobx does, mobxInit
is IMHO not bad. Maybe mobxInitObject
as it deals with a whole object containing observables and other stuff. I don't understand unbox
.
Closing this one now in favor of #2325, to avoid that the discussion happens in two places.
const State = decorate({ value: observable, })(class S<T> { value: T = 0 }) const s: S<number> = new S<number>(...) // : S<number> won't work
@xaviergonz this seems to work (no inference but not terribly annoying)
Enabling
useDefineForClassFields
in TypeScript will prevent decorators from working (both transpiled and usingmobx.decorate
).This flag will be enabled by default for
ESNext
once class fields go stage 4: https://github.com/microsoft/TypeScript/issues/34787Possibly related to https://github.com/mobxjs/mobx/issues/1969
Intended outcome:
autorun
usingobjectState
is executed upon clicking on "increment" button.autorun
usingclassState
is executed upon clicking on "increment" button.autorun
usingclassStateNoDecorator
is executed upon clicking on "increment" button.Actual outcome:
✅
autorun
usingobjectState
is executed upon clicking on "increment" button. ⛔️autorun
usingclassState
is NOT executed upon clicking on "increment" button. ⛔️autorun
usingclassStateNoDecorator
is NOT executed upon clicking on "increment" button.How to reproduce the issue:
https://codesandbox.io/s/fragrant-frog-x2487
Versions
TypeScript 3.7+ MobX - all tested versions (4.x, 5.x)