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
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:
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
Most helpful comment
You can do
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)in both cases invoke it as
Store.someSelector(id). See this commit for an example: https://github.com/mobxjs/mobx-contacts-list/commit/6c8e889f1bc84644d91ee0043b7c5e0a4482195c