Closed AjaxSolutions closed 2 months ago
See: https://mobx.js.org/refguide/reaction.html and https://mobx.js.org/refguide/autorun.html
On Tue, Jun 12, 2018 at 3:41 PM, Les Szklanny notifications@github.com wrote:
I'm new to MST, so forgive me if my question has an obvious answer.
Vue has computed properties similar to MST's views, but it also has watch properties.
If you want to learn about watch properties, here's the Vue doc: https://vuejs.org/v2/guide/computed.html#Computed-vs-Watched-Property
Also, watch this video, scroll to the 28:00 mark: https://youtu.be/UHmFXRp0JDU?t=1691
Would it be possible to add watch properties to MST?
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-state-tree/issues/867, or mute the thread https://github.com/notifications/unsubscribe-auth/AAIrcnkC_h_PNUpYn1Sxqj72a6BWPiDFks5t8CeGgaJpZM4UlHtu .
-- -Matt Ruby- mattruby@gmail.com
Thanks. I think adding reactions/watchers to MST alongside views and actions would be a good feature even though this is already available in MobX.
I think this is a pretty cool idea, because it would save some boilerplate around disposers.
Big question is when should those reactions be setup. afterAttach or afterCreate? AfterAttach is not always triggered (not for roots) or multiple times (when moving nodes, which is not too common), afterCreate however does not give access to parents yet
I was wondering about the same thing recently. I will see if I can PR anything meaningful here.
I'm afraid I will need a hint on why something like this might not be a good idea.
It is not clear how to behave if the property to watch is observable array or map. What exactly should the listener receive? newValue
and oldValue
won't be any much meaningful in such context.
I checked what Vue does in such cases and I think they still supply newValue
and oldValue
despite of them being useless, as they reference the same object.
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
Although not sure if we can do better and keep the immediate simplicity at the same time. @AjaxSolutions what would you expect for example?
Maybe it has sense to provide full change
object instead of newValue/oldValue
pair. Then users could use spread
operators to extract only required properties.
Until someone suggests a better solution I thought it could simply give the full change object to the listener, unless listener explicitly requests two or more arguments (correspondingly newValue
, oldValue
and an optional changeType
).
@mweststrate I guess your comment on https://github.com/mobxjs/mobx-state-tree/commit/1dfe7f4d691b2febe7d9c1fc5ff52157d22d39e2 branch is still very appreciated or anyones with comparable insight into the inner wirings of MST, 'cause I should have overlooked whole lot (types is surely a definite one that I missed).
why not just offer inside self for model types a reaction/autorun method that will add the disposer to some registry that gets disposed on destroy and let the user decide when to call it? (usually inside afterattach or aftercreate)
@xaviergonz what would be the benefit comparing to the solution in the draft? Also don't we have it already through reaction/autorun
imports and addDisposer
method - one could invoke these from whatever place he likes.
If possible, I'd like this feature to work exactly as in Vue, which IMO is easier to use than autorun or reaction.
See the options provided by Vue's watchers. https://vuejs.org/v2/api/#watch
This should cover 80% of use cases. For more complex requirements you can still use autorun or reaction.
See this tweet by the Evan You - the Vue creator. I agree with him that Vue is easier to use than React in part because of watch properties.
https://twitter.com/youyuxi/status/736939734900047874?lang=en
In mobx-react there is disposeOnUnmount
decorator now, which handles similar issue. But in react's observer
components there is no problem with moment when start watching - at component creation you already have the mst tree (or mobx store).
How @mweststrate says, mst node cannot just start watching (I prefer "reacting" word, it closer to mobx universe) at contruction or at afterCreate/afterAttach - because, in general, it can reacts to whole tree, includes parents and neighbors, so node should wait for full ready tree, and, currently, there is simply no way to know about (because it depends on user intentions).
So, what we can do with it? In mobx there is three main concepts: observables, computed and, in end (or in begin?.., huh interesting question), reactions. First two also exists in mst, but for reactions there is no any analogs - we just uses plain mobx reactions, as recommended in docs.
Wait! If mst is living tree, as it described in docs, why it cannot reacts to anything? It's more like a potted tree which dies quickly without the gardener! ;)
I think, lack of side effects tools (I prefer "main effects" word, because, usually, all interesting things are happens here) is big gap for state management library. If we look at redux, there is redux-saga - side effects library. In our main application, which now in migration from redux to mst, we heavily use sagas for big part of business logic, because pure and synchronous way of redux simply doesn't fit the real world.
Of course, bunch of autoruns and reactions in some file can handle all of these effects, but why we cannot embed it into mst? There is pretty straigtforward way to do it:
effects
chainable method, just as actions
or views
. Effect is function which subscribes to something and return itself disposer - right, just as autorun, but also it can be just some addEventListener/removeEventListener pairs or something else. Of course, effects should use actions for tree changing.
types.model('Todo', {
id: types.identifier,
title: types.string,
finished: false,
}).actions(self => ({
update: (newTodo) => Object.assign(self, newTodo),
})).effects(self => ({
notifyBackend: () => autorun(() => api.setTodoFinished(self.id, self.finished)),
listenBackend: () => {
api.listenTodoChanges(self.id, newTodo => self.update(newTodo))
return () => api.stopListenTodoChanges(self.id) //just an example, there is can be some removeEventListener or anything disposer-like
}))
affect(node)
function in mst core, which recursively run all effects in tree and marks node as affected
recursively.unaffect(node)
function in mst core, which dispose all effects in tree and remove affected
mark recursively.isAffected(node)
function in mst core (you guessed, it returns true if node is affected
!).unaffect
node before destroying.unaffect
ed on detach - user can do it himself if needed.affected
node to affected
parent is special case (when user make something like self.todos.push({title: 'New todo'})
, if todos is affected
, new todo will constructed and it's not affected
yet, but it should be affect
ed after attach in most use cases). In such case node will be affect
ed and if user wants to avoid this, he should explicit invokes unaffect(self)
in afterAttach
hook (effects will not even started in that case).I think this concept is simple enough for understanding, implementing and maintaining. What are you thinks? @mweststrate @AjaxSolutions @jayarjo @xaviergonz
Also, affect
and unaffect
can use optional second argument, which disables recursion. For example, on node destroying there is no mean to recursive unaffect
- child nodes just will invoke unaffect
at own destroying.
So, can anybody review my request? I can make a PR, but there is recommendation in readme - discuss extensive changes in issues first.
Some examples. Currently, I have that code (it's with classy-mst, but it doesn't important):
class ItemCode extends shim(ItemData) {
subs: any = []
@action
afterAttach() {
this.subs = [
app.api.chatUpdated(c => c.id === this.id, this.merge),
app.api.messageCreated(m => m.chat === this.id, m => this.insertMessages([m])),
app.api.todoCreated(t => t.chat === this.id, this.addTodo),
]
}
@action
beforeDestroy() {
this.subs.forEach((unsub: Function) => unsub())
}
//...
}
With some sort of autodisposed effects or watchers (which is special case of effect) this code can be rewritten to:
class ItemCode extends shim(ItemData) {
@effect
subscribe() {
return [
app.api.chatUpdated(c => c.id === this.id, this.merge),
app.api.messageCreated(m => m.chat === this.id, m => this.insertMessages([m])),
app.api.todoCreated(t => t.chat === this.id, this.addTodo),
]
}
//...
}
Another effects, which I manage manually now, also can be pulled out to different, explicitly marked as effect, methods, so code will be more transparent and antifragile.
To fix the 'at what point in the lifecycle' problem, would it be fine to set up effects on the next tick?
(also, minor correction on the above reasoning: side effects are first class in MST, through for example actions and flows, just automatic side effects are generalized into their own API)
Next tick after creating? Yeah, it should works. In that case we don't need to explicit call affect
, only unaffect
node after creation if we don't want to start effects automatically.
So,
Todo.create({title})
- effects are started at next event loop.
let todo = Todo.create({title}); unaffect(todo)
- effects will not started until explicit affect
call. Same behaviour if user invokes unaffect(self)
in afterCreate
or, if node attached immediatly after creating, in afterAttach(self)
.
Should we add separated willAffected
function to core for detect planned to affect
nodes?
I like the idea of effects, why the need for names though? it could just be an array (or both object and array could be supported)
About the implementation, why not just hook it into the afterCreate / beforeDestroy events (or afterAttach / beforeDeatch) and if somebody wants something more custom let him do it hooking it himself through any of the other hooks?
With an options argument to effects declaring when you want them to be run
.effects(...., { mode: 'onCreate' | 'onAttach' }) // defaulting to onCreate
if the user wants different effects for different lifecycles he can just call effect twice with different options
Then again I'm not sure how custom events are gonna get along with lazy instantiation.
Just wondering, why do effects need to be run on the next tick? can't they just run right after "afterCreate"/"afterAttach" are finished?
can't they just run right after "afterCreate"/"afterAttach" are finished?
I think you're right, but also there is should be a third mode, manually
or something like it, which runs only at explicit user intention (direct call or affect
at node).
Also, effect should be a functions which returns disposer. In that way user can runs/stops they just as simple functions and it will be easier to stop/rerun all effects on node.
What's the use case for manual triggering?
As for choosing if effects should be run or not it could be done on create
.create(..., {runEffects: true})
// true (or maybe false?) by default
Ok, I think third mode can be added after some battle testing, if needed. runEffect should be true by default, because create can be called implicitly at snapshot converting.
Ok, a couple of points, I'd totally not have a mode (create or after attach), and this is why:
runEffect for implicit conversions could be inherited from the value when it was used on create of the parent e..g
const m = X.create(..., {runEffects: true}}/ force run on itself and children that do not specify a preference
m.child = {...} or Child.create() // effects will run (inherited)
m.child = Child.create(..., {runEffects: true}) // will run for itself and children that do not specify a preference
m.child = Child.create(..., {runEffects: false}) // won't run on itself and children that do not specify a preference
const m = X..create(..., {runEffects: false}) // force not run on itself and children
m.child = {...} or Child.create() // effects won't run (inherited)
m.child = Child.create(..., {runEffects = true}) // will run for itself and children that do not specify a preference
m.child = Child.create(..., {runEffects = false}) // won't run on itself and children that do not specify a preference
and if there's no runEffects set and no parent the it will assume undefined
why undefined?
const parent = Parent.create(..., {runEffects: true}) // effects will be run as requested, in this case as part of "afterCreate"
const child = Child.create(...) // inherit, but we don't know if it will be a root node or not, but since the default is undefined we won't run them right now
parent.setChild(child) // aha, now we know what the inheritance asks of it, so we run the effects (albeit admittedly as part of an "attach" event
summarizing:
The good thing is that this would be possible:
Basically it is done for you and you don't have to think about it. The usual would just be to set runEffects to true upon creating the root node of the store.
I hope that makes sense.
That being said, wouldn't the api be more clear like this?
types.model(...)
.reaction((self) => self.id, (self, newValue) => { do whatever }, reactionOptions?)
.autorun((self) => { do whatever }, autorunOptions?)
.customEffect((self) => { return a disposer }, options?)
that'd be more akin to watch properties too
Also I'd also maybe add those methods to any complex type (map, array, model, etc), not only models (but of course not to simple types such as string), but that could be left for the future since those types don't have hooks right now.
And I guess the clone method would need options with runEffects too :)
Instead of customEffect
there is can be just effect
and reaction, autorun etc (onAction, may be, as suggested in #1056?) it's just sugar for effect
. Also, we need functions for start/stop effects manually (what if we detach node and want to effects don't stop, or stop effects on some subtree?) and one for detecting if effects are running.
I do incline to the latter proposal with API clearly mirroring mobx's one - it's dead simple and gives reasonable flexibility.
Single setting (runEffects
) looks acceptable and I can understand it's use-cases, although same result could be achieved by proper composition.
Best place to run effects seems to be ObjectNode.finalizeCreation
. We could introduce afterFinalized()
hook (as the last meaningful action of the method) and use it internally.
Can't think of any use cases for starting/stopping effects manually. It would be great to have an example.
I guess customEffect could be just effect yes
@k-g-a although same result could be achieved by proper composition.
what do you mean?
Anyway we need some internal function to start/stop effects. Why don't give it to users? I can imagine what some user need to detach some existing subtree and run effects on it again, or disable effects in subtree under some circumstances.
We're using some mobx effects (autorun, reaction...) in afterCreate
but when we upgraded to MST3 we found some of them weren't triggered on creation but on access, due to the lazy mode introduced in MST3. That introduced some bugs and we had to "force" the access to launch those afterCreates
.
Don't get me wrong, lazy mode is awesome (out .create()
now takes 6 times less!) but it doesn't play nice with effects. Maybe if runEffects
is true it should disable lazy mode? Or maybe a new option should be added, lazy
, true by default so devs can control it?
By the way, following this reasoning, afterCreate
in lazy mode is a bit confusing. Maybe afterAccess
or afterFirstAccess
would be easier to understand.
Ehm... I still miss a point sorry. Has anyone here looked at this branch? And if yes, could you please explain to me:
addDisposer
to the internal cache, that is auto-purged when node is destroyed, so that's good?Effects is more general than watchers, so we can implement watchers with access to old values on effects base. Dedicated effects also removes some boilerplate and code will be more decoupled (effects will not explodes afterCreate
, but placed at own section).
@jayarjo it is certainly more akin to watch props, but what if you want to react to a child component prop from the parent?
For example, say that you want to trigger a reaction inside the todoStore when a todo is added (like making a rest call to save it on the server)
Then also, you might want to disable that API call when you are inside tests (although I guess a way around this would be to use getEnv to avoid those)
As for attaching reactions when a node is instantiated, I guess that makes sense at first, but might be reacting to "initialization" changes in an "unfinished" node since there might be be more init code inside those hooks.
Dunno, generalizing this is hard 💃
I'm starting to think it is just easier to have effect
function that returns a disposer for early disposing and where the automatic disposing gets triggered depending on where you call the function
.actions((self) => {
// created here (initialization phase) means auto dispose on beforeDestroy, ran before afterCreate/afterAttach
effect(self, reaction(.....))
return {
afterCreate() {
effect(self, reaction(...)) // will auto-dispose on beforeDestroy
},
afterAttach() {
effect(self, reaction(....)) // will auto-dispose on beforeDetach
},
someOtherAction() {
effect(self, ....) // or anywhere else: throw? allow and dispose on beforeDestroy to allow adding effects to a node when already created?
}
}
})
where effect can also be used for custom stuff
effect(self, () => { ... whatever returns a disposer })
and if the effect has to run only on certain conditions the user can just if/else based on getEnv, a property, a property of the root, process.env.NODE_ENV or whatever he wants
this would effectively be a "smarter" replacement of addDisposer
@xaviergonz I mean that if one describes effects on model type declaration, why would he want to turn those on or off?
const MyModel = types.model({props}).views(...).actions(...);
const MyModelWithEffects = MyModel.reaction(...);
/* or even */
const MyModel1 = types.model({props});
const MyModel2 = types.model({props});
const MyEffects = types.model().reaction(...);
const MyModelWithEffects1 = types.compose(MyModel1, MyEffects);
const MyModelWithEffects2 = types.compose(MyModel2, MyEffects);
/* and then instead of calling */
MyModelWithRunEffectsSetting.create({snapshot}, {runEffects: CONDITION})
/* one could just */
const Factory = CONDITION ? MyModelWithEffects1 : MyModel1;
Factory.create({snapshot});
Such approach makes code more self-documented as it implies concrete knowledge whether this particular instance contains effects or not. Meanwhile relying on ancestor's setting up the tree you will end up debugging your "supposed-to-run" effects in runtime - just to find out which of those ancestors accidantaly turned them off. Additionally, using composition will give you proper types for free: effects will be listed for models which have those and won't - for the rest.
For those who really want to have such a 'setting', it's not hard to implement (pseudocode):
const AutoEffectsModelBase = types
.model()
.volatile(self => ({
__effectsEnabled: true
}))
.actions(self => ({
afterCreate() {
getMembers(self).effects.forEeach(key => {
const originalEffect = self[key];
self[key] = (data, reaction) => !self.__effectsEnabled && originalEffect(data, reaction);
})
},
//automatic, can be enhanced in any way (i.e. through getEnv() or process.env.NODE_ENV)
afterAttach() {self.__effectsEnabled = true},
beforeDetach() {self.__effectsEnabled = false},
//manual
enableEffects() {self.__effectsEnabled = true},
disableEffects() {self.__effectsEnabled = false}
}))
@jayarjo , considering the branch you've mentioned: it narrows down every particular reaction to one field. So if one needs to react to subset of fields, he must first define a computed property (view
), which just uses all the data needed, and then add a reaction to that view
. This is actually not far from custom reaction
at afterCreate
.
@k-g-a OK, maybe custom cases can be left for custom implementations, and certainly using models with and without effects can be controlled by using an extended model or not.
(still for these custom cases I think adding to add Disposer a parameter where you can configure on which phase they will be disposed would be cool 😎)
but on which lifecycle phase should they be started in your opinion?
'Additionally, using composition will give you proper types for free: effects will be listed for models which have those and won't - for the rest.'
why would effects need to be in the typings? they are kind of internal functions (unless you don't mean model instance typescript typings 😁)
off topic : maybe there could be an afterInitialized hook with docs rather than expect people to know that stuff that runs on the top of views / actions /extends functions run on that phase?
So you guys are looking for something more exhaustive, than what this thread was originally about. I think that anything more complex than what I referenced in the branch doesn't worth the alternative implementation and will only confuse things.
afterCreate/afterAttach() {
addDisposer(self, autorun/reaction(...));
}
Is already simple enough for all the cases beyond watching separate properties. IMHO.
I think it's a good opportunity to create an easier API for a common pattern :)
And maybe solve the lazy problem along with it.
Main purpose of separated effects is improved readability and structure of code, I think.
Okay, maybe we went too deep for beginning of such complicated feature? Lets done something stupid and obvious and then improved that when users will complain about.
For example, only add chainable effects
function, which is tiny wrapper on top of actions. User can calls effect (just as action) and it will automatically scheduled to dispose on destroy. Also on effect in runtime there is running
property and stop()
function, so user can manage them manually. He can run it on lifecycle hooks or from actions and stop if needed.
And, if there is effects
on model, it's not lazy
.
something like:
.effects(self => ({
whateverFx: reaction(() => self.foo, () => {...})
}), { autoStart: true/false} /* defaults to true*/)
and then access them through
getEffects(node).whateverFx.(running/stop()/start()) ?
??
About lazy, makes me wonder: Since effects actually access props they want to react to and props accessed are auto-instantiated, does it really matter if they are lazy?
Also, again, what's a use case for manual stop, checking if it is running, etc? I'd love to see an example.
No, even more stupid.
.effects(self => ({
whateverFx: () => reaction(() => self.foo, () => {...})
})
.actions(self => ({
afterAttach() {
self.whateverFx() //run the effect, schedule it to be disposed on destroy
},
someAction() {
if (self.whateverFx.running)
self.whateverFx.stop() //dispose effect
//else effect will be disposed on destroy
}
}))
In that case all is very explicit and user can do all sort of things that are possible with raw reactions, but effects are decoupled from actions and there is ways for next improving.
fair enough by me :)
just a small change
self.whateverFx.start() //run the effect, schedule it to be disposed on destroy
since in the mobx world calling a function is usually akin to disposing
my only reservation is that I don't see it much of a change over self.addDisposer
May be, not the point.
пт, 26 окт. 2018 г., 1:53 Javier Gonzalez notifications@github.com:
fair enough by me :) just a small change self.whateverFx.start() //run the effect, schedule it to be disposed on destroy
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-state-tree/issues/867#issuecomment-433201766, or mute the thread https://github.com/notifications/unsubscribe-auth/AElX2XX1FfgGiXkynsAE8D1GVEjKGHtXks5uoiTWgaJpZM4UlHtu .
afterAttach() {
self.whateverFx.start()
},
The problem if we rely on afterAttach
is that there's no way to be sure that an effect will run after model creation.
About lazy, makes me wonder: Since effects actually access props they want to react to and props accessed are auto-instantiated, does it really matter if they are lazy?
That makes perfect sense, the effect could observe the props of the instance and therefore that instance would be created/attached. So no need to mark it as no-lazy by default.
– But this won't work if we rely on afterAttach
as I explained above.
This is why i wrote about disabling lazy mode if effects are declared on model. So, basically, effects doing 2 things - more decoupled code and solves lazy problem.
I'm not sure if disabling lazy mode when effects are present is the desired behaviour. Imagine this pattern:
const Parent = types.model('Parent', {
children: types.array(Child),
allChildrenActive: false
})
const Child = types.model('Child', {
active: false
})
.effects(self => ({
activateOnParentActivation: autorun(() => {
if (getParent(self).allChildrenActive === true)
self.active = true
})
})
Sorry not to come up with a more realistic example, but if I'm not mistaken this effect
would instantiate the parent but none of the children (as long as they are not observed) because the effect is observing the parent and not itself. So, it would still work fine, but it'd be more performant as the laziness would be still linked to runtime observation.
I think that there is no place for laziness if any side effects in a game. But for similar cases there is can be some sort of immediate-on-instance-effects.
I'm new to MST, so forgive me if my question has an obvious answer.
Vue has computed properties similar to MST's views, but it also has watch properties.
If you want to learn about watch properties, here's the Vue doc: https://vuejs.org/v2/guide/computed.html#Computed-vs-Watched-Property
Also, watch this video, scroll to the 28:00 mark: https://youtu.be/UHmFXRp0JDU?t=1691
Would it be possible to add watch properties to MST?