question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Question: side effects for caching in @computed values with existing observers

See original GitHub issue

Problem

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 that invalidateCache 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 as onClick={() => this.props.injectedCache.value = 'test'} in a React render() function. It seemed like adding dumb @computed get/set wrappers around my @observables seemed to solve the issue, although I haven’t attempted to debug this any further as to see why.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:20 (7 by maintainers)

github_iconTop GitHub Comments

2reactions
mweststratecommented, Feb 25, 2019

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.

2reactions
mweststratecommented, Feb 22, 2019

TL,DR:

import { observable, computed, action, _allowStateChanges } from "mobx"

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!
     _allowStateChanges(true, () => {
        this._cachedValue = someApiCallToGetValue();
     })
    }
    return this._cachedValue;
  }

  @action invalidateCache = () => {
    // value lazy-loads again next time computed value is observed
    this._cachedValue = undefined;
  }
}
Read more comments on GitHub >

github_iconTop 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 >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found