Question: side effects for caching in @computed values with existing observers
See original GitHub issueProblem
hi! I’m new to mobx and am trying to implement caching for my application’s state using @computed
values but am running into an error. I have a contrived example that hopefully demonstrates my intent:
class Cache {
@observable _cachedValue;
@computed get value() {
if (!this._cachedValue) {
// for context, this is a call into a native node module's synchronous API
// to retrieve some data - no fetches or promises here!
this._cachedValue = someApiCallToGetValue();
}
return this._cachedValue;
}
@action invalidateCache = () => {
// value lazy-loads again next time computed value is observed
this._cachedValue = undefined;
}
}
On the initial observe or when the cache is invalidated via invalidateCache
, the computed value should refresh the @observable
with someApiCallToGetValue()
as a side effect before returning it.
I get the following error when I use this approach however:
Error: [mobx] Computed values are not allowed to cause side effects by changing observables that are already being observed.
Question
Is it possible to accomplish my desired side-effect in a computed value with existing observers? If not, is there a recommended alternative approach I should be taking?
Alternatives considered
- It’s been mentioned in #307 that a lazy-loading pattern with intentional side effects is useful and can be achieved with onBecome(Un)Observed events added in #323, but it doesn’t seem to solve this issue as I need to have my computed value able to achieve a side effect while being observed.
- Another alternative after looking at #1534 and #1799 was to use
@computed({keepAlive: true})
so that the computed value itself becomes the cached value. However it seems like this would require the usage of a dummy@observable
referenced by my computed value so thatinvalidateCache
can update my computed value, which is very hacky and doesn’t seem like an intended use case. - I’ve tried using ES6 proxies which were also mentioned in #307. I ended up with a proxy that handled lazy-loading and cache invalidation around the original cache which now only contained the
@observables
and nothing else. but I was seeing the following error:Error: [mobx] Side effects like changing state are not allowed at this point. Are you trying to modify state from, for example, the render function of a React component?
when attempting to interact with my proxy in simple use cases such asonClick={() => this.props.injectedCache.value = 'test'}
in a Reactrender()
function. It seemed like adding dumb@computed
get/set wrappers around my@observable
s seemed to solve the issue, although I haven’t attempted to debug this any further as to see why.
Issue Analytics
- State:
- Created 5 years ago
- Comments:20 (7 by maintainers)
Top Results From Across the Web
Mobx how to cache computed values? - Stack Overflow
Note that computed supports a keepAlive option, that will force mobx to cache the value, even when there are no observers.
Read more >Deriving information with computeds - MobX
Computed values can be used to derive information from other observables. They evaluate lazily, caching their output and only recomputing if one of...
Read more >(@)computed · Mobx Doc - iiunknown
Computed values are values that can be derived from the existing state or ... For example imperative side effects like logging, making network...
Read more >(@)computed - MobX
Computed values are values that can be derived from the existing state or ... For example imperative side effects like logging, making network...
Read more >Understanding MobX and MobX State Tree - Medium
Computed values are updated lazily. Any computed value that is not actively in use will not be updated until it is needed for...
Read more >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Ha MobX it’s even more protective about keeping it’s mental model of a pure flow of state -> derivation -> effect. I tried tricking it by simply dropping the
@computed
decorator (since it is just caching a property lookup anyway, it is not adding much). And then React started to throw, reminding me why it is good to stay to the pure model 😃.So if this warning isn’t throw, React will start to generate warnings for exactly the same reason. Because the data flow becomes
render -> read value -> change cache -> MobX detects change in cache -> MobX schedules a render -> React prints a warning: A render caused a render, so there are side effects!
.Going down that path made me realize we are looking in the wrong direction. We don’t want to cause a change by loading, instead, we just want to abstract away whole the loading part and build our own observable concept; the cache. The right building block for creating our own custom observable data structures is createAtom; all observable datastructures are based on that, see: https://mobx.js.org/refguide/extending.html
Anyway, realizing that it become immediately quite simple, as demonstrated here:
https://codesandbox.io/s/pwl87n09kq
A neat benefit of createAtom, is that it allows you actually to hook into when an observable is first, or no longer used, and we can use that to potentially automatically discard the cache, as demonstrated here:
https://codesandbox.io/s/2olv8v8090
Sorry for realizing this so late, was for too long just staring at how to solve your code, rather than how to solve your problem 😃.
The clue is that our model is now still pure: reading state only observes things. We report that our cache has been used. Writing the cache, reports a that the concept of our cache has changed.
TL,DR: