xaviergonz / mobx-keystone

A MobX powered state management solution based on data trees with first class support for Typescript, support for snapshots, patches and much more
https://mobx-keystone.js.org
MIT License
549 stars 25 forks source link

addDisposer analog #171

Closed Amareis closed 4 years ago

Amareis commented 4 years ago

Hi! Currently I translated out project from mst to keystone, and we actively using addDisposer. Does it possible to implement it in keystone?

xaviergonz commented 4 years ago

Since nodes in mobx-keystone cannot become dead there's no direct equivalent. However you can use the hook that runs whenever a node is detached from a root store node (the function that you return from onAttachedToRootStore) (https://mobx-keystone.js.org/rootStores) or alternatively onChildAttachedTo (https://mobx-keystone.js.org/treeLikeStructure#onchildattachedtotarget---object-fn-child-object----void--void-options--deep-boolean-fireforcurrentchildren-boolean--rundetachdisposers-boolean--void) to kind of emulate this.

Amareis commented 4 years ago

Yep, I already make such wrappers:

import {AnyModel, getRootStore, model, ModelClass} from 'mobx-keystone'
const $disposers = Symbol('disposers')

export type Disposer = () => void

export const coolModel: typeof model = name => clazz => {
    if ('onAttachedToRootStore' in clazz.prototype)
        throw new Error("Don't use onAttachedToRootStore!")
    clazz.prototype.onAttachedToRootStore = function onAttachedToRootStore(root: any) {
        this[$disposers] = [] as Disposer[]
        this.onAttach?.(root)
        return () => {
            this[$disposers]?.forEach((d: Disposer) => d())
            delete this[$disposers]
        }
    }
    return model(name)(clazz)
}

export function addDisposer(node: AnyModel, disposer: Disposer) {
    if (!($disposers in node))
        throw new Error('Model is not attached to root store!')
    // @ts-ignore
    node[$disposers].push(disposer)
}

I think, set of such wrappers can be published as package for tiny mst-compatibility layer, #156 related

Amareis commented 4 years ago

This snippet works, but does disposer from onAttachedToRootStore is running after actual detaching? It's not good, child node sometimes need to use some root node methods to correct dispose itself.

xaviergonz commented 4 years ago

Yes, it runs after being attached and after being detached. Since it runs as a disposer and the hook takes the root store it gets attached to as a parameter you can still call root store methods on those. e.g.

onAttachedToRootStore(rootStore) {
 return () => {
    // already detached, call old root store method
    rootStore.whatever(this);
  };
}

Have you found a use case that is not covered with this pattern?

Amareis commented 4 years ago

Have you found a use case that is not covered with this pattern?

Nope, I adapt my code to store root reference too. So, basically, addDisposer can be easily implemented in user-land.