-
Notifications
You must be signed in to change notification settings - Fork 642
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: types.hooks / TYPE.withHooks #1151
Comments
Related issue: #947 |
A risk might be performance, previously, all values in MST were backed by the general concept of 'node' that took care of all the householding around values. This was split into scalarnodes and objectnodes, that significantly improved performance; scalarnodes are pretty dumb and used for leaf values (primitives / frozen), and object nodes are still pretty dynamic, being able to perform all the chores around hooks and such. The risk of introducing hooks for scalars could be to loose a bit of those performance improvements. |
Making hooks as a separate wrapper type is a viable idea in general. To my opinion,
I'd suggest to leave hooks for ComplexTypes only (model/map/array) in form of
|
BWT, as we touched the performance: internal I mentioned it not to blame anyone (including myself, as I saw the review :)), but to point out that little drops make the mighty ocean: adding several non-significant (in terms of performance) features may sum up into noticable problems. |
I really like the idea of having hooks separated from actions with their own Two suggestions:
.hooks({
preProcessSnapshot(sn) {},
postProcessSnapshot(sn) {},
afterCreate(self) {},
afterAttach(self) {},
beforeDetach(self) {},
beforeDestroy(self) {}
})
We talked about that with more detail here in the effects issue: #867 |
Thanks all for the input! After thinking some more about it I think pre/post process snapshots functions should be separated from the other hooks just for exclusively typescript reasons. Right now models have to use a hideous "CustomC, CustomS" template parameters just because they are included with everything else, but if they were in a separate method they could be typed much more easily: function types.snapshotProcessor<C, S, T, C2 = C, S2 = S>(
// plus optional new name
type: IType < C, S, T >,
processors: {
preSnapshotProcessor?(sn: C): C2,
postSnapshotProcessor?(sn: S): S2,
}
): IType<C2, S2, T> That would avoid the mess that is needed right now in the typings to make those work (and make the typings easily adapt to any kind of type). Also, I'd make it so at least the pre/postProcessor hooks can also be used on scalar nodes, since they can become really handy for transformations (for example transform strings to numbers, strings to other strings, null to undefined, etc). About the performance, I think that can be easily fixed by "proxiing" the current EventHandler methods through some other methods that will create the EventHandlers object lazily if needed. e.g.
About the other hooks, yeah, I guess they really aren't that useful on scalar nodes and can be added at a later stage (if ever needed), but with the above approach to fix the performance it shouldn't hurt. @luisherranz If hooks are made a plain object then there's no way for them to share state, e.g. .hooks({
afterCreate(self) {
// I create here something that needs to be disposed on destroy
},
beforeDestroy(self) {
// how to access that thing created on afterCreate here?
},
}) while with either of these two solutions it is possible .hooks(() => {
let whatToDispose
afterCreate(self) {
// whatToDispose = ...;
},
beforeDestroy(self) {
// dispose whatToDispose
},
})
.hooks((self) => {
let whatToDispose
afterCreate() {
// whatToDispose = ...;
},
beforeDestroy() {
// dispose whatToDispose
},
}) and while it could be argued that there's "addDisposer" for that, we might be loosing some other use cases there. About if self should go in |
Btw, instead of adding a new hook (before/afterInitialize), what about doing something like this?
Where when creation mode is set to "eager" it will ensure that the node (plus all the subtree to the root) is forcedly created once the whole tree is ready. Basically its implementation would be to hook to the internal This could be also how probably effects would be created internally (since effects require a "built" node anyway and most probably you want to ensure that if there's an effect set it will run no matter if the node is first accessed or not) |
You're right.
Seems fine to me. |
Seems fine, as it was discussed at #947.
As I mentioned before, we could implement both (plainobject/delegate) so user could decide if he needs some local state between hooks or simpler code.
I'd prefer to keep things as simple as possible, without giving many options to end-user. Ideally, things ought to "just work", without even knowing whether its lazy or not. It seems like
Laziness does not solve the problem, it jsut defers it. Same as it happens with lazy |
Just wondering, would it be possible to implement a
Sounds good, though I'm struggling to understand the difference between the proposed "afterInitialized" (is it after the whole tree has been created but before the node has been created?),"beforeInitialized" (is there even an instance available before initialization? is it actually before lazy creation or after lazy creation?), etc.
Hmm, wouldn't in this case? It would not create any EventHandlers object at all if no hook handlers are registered (which probably would be the case for 99% of scalar nodes) |
Sorry, I missed that. Perfect solution.
This would be ideal. What about |
Observed makes me think there's a reaction or something attached to the object. I also though of |
I like the afterCreate(getSelf) {
const self = getSelf();
// do stuff with the initialized self.
} That way you don't need to initialize the node if you don't need it! |
Still it would need a way to get when the node is lazily created in case that's what the user want async afterCreate(getSelf, getLazySelf) {
// do stuff before node is actually created (akin to beforeInitialize)
// choose one or none
const self = await getLazySelf(); // will keep running once self is lazy created (akin to current afterCreate)
const self = getSelf(); // will create ("access") the node before returning it (akin to some new eager afterCreate)
// node is ready
} |
Actually we could even take a clue from react hooks and manage the whole creation/destroy cycle in a single place lifecycle({onCreate, onAttach, onFinalize}) {
// do stuff before node is actually created, but (akin to beforeInitialize)
// optional
onCreate((self) => {
// node is ready (afterCreate), but not yet attached
// optionally return a disposer that would be beforeDestroy
return () => {};
});
// optional
onAttach((self, parent) => {
// node is ready, it has been attached to a parent (afterAttach)
// optionally return a disposer that would be beforeDetach
return () => {};
});
// optional
onFinalize((self, parent?) => {
// node is ready, it has been created and possibly attached (unless it is a root node) (afterCreationFinalization)
// this is probably what 99% of the people would use
// optionally return a disposer that would be also beforeDestroy
return () => {};
});
return 'lazy' | 'eager'; // return creation (touch) mode, eager or lazy
// in eager mode the node will be "accessed" as soon as the root node is finalized
} Also sharing state should be easier |
|
Yeah, lifecycle chaining should be possible. Effects could be modeled over hooks indeed, but still I see them as a valuable addition. |
In the end the lifecycle is very close to hooks, so might as well be... // call as function or with a plain obj
hooks(() => {
// do stuff before node is actually created, but (akin to beforeInitialize)
return {
// all is optional
onCreate((self) => {
// node is ready (afterCreate), but not yet attached
// optionally return a disposer that would be beforeDestroy
return () => {};
}),
onAttach((self, parent) => {
// node is ready, it has been attached to a parent (afterAttach)
// optionally return a disposer that would be beforeDetach
return () => {};
}),
// optional
onFinalize((self, parent?) => {
// node is ready, it has been created and possibly attached (unless it is a root node) (afterCreationFinalization)
// this is probably what 99% of the people would use
// optionally return a disposer that would be also beforeDestroy
return () => {};
}),
// return creation (touch) mode, eager or lazy
// in eager mode the node will be "accessed" as soon as the root node is finalized
creationMode: 'lazy' | 'eager'
};
}); if multiple hook calls are chained they all will be called in order, where creation mode will be lazy only if all of them are set to lazy |
@mweststrate @k-g-a any thoughts? |
Really like where this is going. Not sure about the 'best' api yet. A consideration: I think hooks and actions quite often want to close over the same state, so we have to make sure |
The problem I see with hooks inside extend is that it currently gives you access to self, so there would be no way to make a beforeInitialize or any optimizations if a plain object is used instead to declare the hooks. Also extends kind of suffers from the issue that its typings kind of allow you to easily skip computed views.
vs
so maybe it could be something like
Of course if the user doesn't want to use localState at all then he'd just not use the parameter and omit it, so it would be backwards compatible |
Although now that I think about it, since views is an object returned in extends, it could be patched so its properties are substituted by their computed versions, and same with the actions, volatile, etc. If that's done then using extends exclusively should be OK (except for the part where it requires self, so no beforeInitialize, but then again I'm not sure how useful beforeInitialize would be anyway). |
Looks good to me. Do I get it right, that you just omitted beforeDetach/beforeDestroy from example above, but those are supposed to be present? ) |
Before detach and before destroy would become optional disposers returned from the initiators as shown in the example actually, but that can be switched back easily if you think discrete events are better |
So the rule would be "a function returned from the hook will be called at corresponding 'closing' step". My argumants against are: sounds like a magic rule, seems complex to teach, differs from current approach. |
Is there any chance this discussion will be revived or are custom type hooks dead? Asking because I can't think of a way of having a type that refreshes its value automatically based on another observable (think: date time type that needs to update the timezone) without traversing entire tree and doing it manually and that feels icky :) |
Right now hooks / snapshot pre/post processing can only be done over models, but what if it could be applied to any kind of type node with something like this:
With that in place, doing something like a
types.optionalNull
would be kind of trivial:Also this could coexist with the current way until (if?) the hooks inside model actions / .pre-.postProcessSnapshot get deprecated in a future v4
The only con I can see is that the hooks would no longer be able to access "internal state" on the model, e.g.
But for that something like
could be created
Optionally it could also deprecate types,refinement by adding a validation function to the hooks themselves
Thoughts?
The text was updated successfully, but these errors were encountered: