Mobx: computed properties (methods) with arguments

Created on 2 Jun 2016  ยท  13Comments  ยท  Source: mobxjs/mobx

How could it be done with mobx?

class Store {
@observable model = {};

@computed get someSelector() {
    return (id) => {
        const value1 = this.model.list1.find(item => item.id === id).value
        const value2 = this.model.list2.find(item => item.id === id).value
        return value1 + value2
    }
}

I connect that someSelector to react component and would like component to update whenever items in lists with certain ids are changed.

In real app, computation may have dependencies on observable data of different level of store and it is not very convenient to have computed properties on item levels.

I tried to return asReference(() => ...), but this doesn't work for me either

โ” question

Most helpful comment

You can do

get someSelector(id) {
   return computed(() => someExpr).get()
}

or, more efficient, use createTransformer (which is basically the same as the above, except it memoizes; it keeps a map id -> computed, so for the same id you get the same computed back)

public someSelector = createTransformer(id => someExpr)

in both cases invoke it as Store.someSelector(id). See this commit for an example: https://github.com/mobxjs/mobx-contacts-list/commit/6c8e889f1bc84644d91ee0043b7c5e0a4482195c

All 13 comments

You can do

get someSelector(id) {
   return computed(() => someExpr).get()
}

or, more efficient, use createTransformer (which is basically the same as the above, except it memoizes; it keeps a map id -> computed, so for the same id you get the same computed back)

public someSelector = createTransformer(id => someExpr)

in both cases invoke it as Store.someSelector(id). See this commit for an example: https://github.com/mobxjs/mobx-contacts-list/commit/6c8e889f1bc84644d91ee0043b7c5e0a4482195c

@mweststrate Alright, gonna try it and see how it works. Ideally I'd like to have the same opportunity of writing complex selectors that I have with reselect.

In computed properties you can basically do anything except modifying state so you should be fine there. Common practice in MobX is to not normalize data, so usually normal objects are passed around instead of ids. But more importantly the computed properties are usually defined on the object that owns or deletes them. So where in Redux you would have a selector (factory), in MobX people either introduce a computed on an instance field, or as field in a React component. And often even just plain functions are used. Things do not have necessarily to be computed, it is merely an optimization. As long as the function are invoked from a "reaction" such as observer, reaction, when or autorun things will remain reactive.

If you are stuck at anypoint, feel free elaborate on your scenario. People are here to help :)

@mweststrate Yeah, I thought about to create computed property on nested object levels, but there are few reason that I want to do it:

  1. Computed data property may not belong to a single object, it may aggregate data from different levels of data
  2. It's not very convenient to wrap plain objects into models with computed properties.
this.model.list.forEach(item => { item.data = new Model(item.data) })

Probably I get it wrong.

Fair enough, in that case I would simple introduce methods like

selectorFactory(id) {
    return computed(() => {
        const value1 = this.model.list1.find(item => item.id === id).value
        const value2 = this.model.list2.find(item => item.id === id).value
        return value1 + value2
    })
}

or even

selectorFactory(list1, list2, id) {
    return computed(() => {
        const value1 = list1.find(item => item.id === id).value
        const value2 = list2.find(item => item.id === id).value
        return value1 + value2
    })
}

From there on you can instantiate selectors by invoking the factory, and get the result of the selector by using .get()

Yeah, right, seems like good approach.
It there any way to cache the result of computation with different inputs?

I tried createTransform but it caches the result only until it called with another arg.

@satispunk that happens automatically if you use the computation in some reaction like autorun or observer (egghead lesson explaining that is almost up :)).

@mweststrate I guess we are talking about different things:

selectorFactory(list1, list2, id) {
    return computed(() => {
        const value1 = list1.find(item => item.id === id).value
        const value2 = list2.find(item => item.id === id).value
        return value1 + value2
    })
}

Each time we call selector it creates new computed object/value, if we have few components that call selector with the same arg the inner expression will be called several times.

So I would like somehow to store created selectors on the fly.
It would be not very efficient to create all of them in constructor of the store and some of them may not be called at all.

I can see why you want this pattern when coming from Redux, but I have to admit personally I never needed a pattern like this, where I couldn't store the computed either on the component or on model data. I'm curious to the bigger picture, maybe I would approach the whole problem entirely different?

That being said, if you really want / need this pattern, I think you should just build a memoization map with a refcount (to clean up stuff again). Hack that is probably even a generic pattern (and a generalization of createTransformer). The memoization function should be really straight forward, you only have to compare all input arguments on pointer equality. That is quite a cool idea actually, maybe MobX should offer something standardized for that :-D.

@mweststrate yeah, I tried something like

memoize(selectorFactory(list1, list2, id) {
    return computed(() => {
        const value1 = list1.find(item => item.id === id).value
        const value2 = list2.find(item => item.id === id).value
        return value1 + value2
    })
})

but it does not work well if component is unmounted and the mounted again (ie virtual list) even state is not changed. I have looked at the code and seen that it resets value if there is no more observers, right?

I started think that it should not be computed value but only selector with memoization, but somehow it should know when state is changed and it should recompute value and memoize it again. Probably reaction should be created in store constructor but then it should be disposed when store is not in use anymore

Does it make sense?

Is this still an issue?

I have also come across this pattern and I am wondering now why the transforming function requires an object as argument as opposed to a plain string or number. The string or number would itself serve as an memoization key.

Same question than @timeyr

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ariona picture ariona  ยท  3Comments

ansarizafar picture ansarizafar  ยท  4Comments

bakedog417 picture bakedog417  ยท  3Comments

giacomorebonato picture giacomorebonato  ยท  3Comments

joey-lucky picture joey-lucky  ยท  3Comments