mobx icon indicating copy to clipboard operation
mobx copied to clipboard

`requiresReaction` in options of `computed` does not throw as indicated by the documentation

Open upsuper opened this issue 1 year ago • 7 comments

import * as mobx from 'mobx';
const a = mobx.computed(() => console.log('b'), { requiresReaction: true });
mobx.onBecomeUnobserved(a, () => console.log('c'));
a.get();

The document says:

requiresReaction

It is recommended to set this one to true on very expensive computed values. If you try to read its value outside of the reactive context, in which case it might not be cached, it will cause the computed to throw instead of doing an expensive re-evalution.

Intended outcome:

It should throw, because it is not called within a reactive context.

Actual outcome:

It does not throw.

How to reproduce the issue:

Just run the code above.

Versions

MobX 6.

upsuper avatar Jan 30 '23 22:01 upsuper

I noticed that this behavior was changed in #2998 possibly intentionally.

But as I mentioned in https://github.com/mobxjs/mobx/issues/2417#issuecomment-1341584660 previously, we in Canva find it hard to enforce restrictions if they don't actually throw, because developers don't look at console all the time when nothing goes wrong, and it is hard to track all the requirements across a large codebase. So we would prefer there to be a way to enforce the restrictions.

If it is intended to make requiresReaction on par with behavior of other requires* to warn, it would be better to introduce a new enforcesReaction to actually throw.

upsuper avatar Jan 30 '23 22:01 upsuper

There is also another potential use case of requiresReaction: computed may sometimes be used for managing resources, which may be leaking, on demand. To prevent leaking, we may use onBecomeUnobserved to clean up. However, it doesn't work outside observed context, so one may want to enforce reaction for that reason.

An example as below:

let _blobUrl: string | undefined;
const blobUrl = mobx.computed(() => {
  // some check
  if (_blobUrl != null) {
    URL.revokeObjectURL(_blobUrl);
  }
  // generate a blob
  _blobUrl = URL.createObjectURL(blob);
  return _blobUrl;
});
mobx.onBecomeUnobserved(blobUrl, () => {
  if (_blobUrl != null) {
    URL.revokeObjectURL(_blobUrl);
    _blobUrl = undefined;
  }
});

However, if you call blobUrl.get() outside observed context in a one-off way, the URL it generates will not revoked by onBecomeUnobserved as expected.

upsuper avatar Jan 30 '23 23:01 upsuper

Do I follow correctly it actually reports, so it's only about warn vs throw?

As a workaround you can patch console.warn and use regex to intercept what's needed.

I would like to keep it consistent. I also prefer if errors are sound and basically non-ignorable, but one thing to consider is that these checks are dev only - conceptully I think it's a bit weird if something is considered an error in dev, but not on prod. IIRC React also just logs in most of the "suspicious" situations. Also errors are often thrown from async contexts, so they end up only in the console anyway.

Maybe we could introduce report/warn config option, which defaults to console.warn: configure({ report: (msg) => { throw new Error(msg) } }) ?

urugator avatar Jan 31 '23 12:01 urugator

There are many warnings that should be fixed, like React key warnings, that are not errors to not deviate from production behavior, as otherwise bug reproductions can become extremely hard. If checking warnings is not part of the developers typical habits before putting up a PR, I'd indeed solve this as the infra level of your product, and raise on-screen alerts on any logged warning/error you believe must always be fixed in your company.

That being said, I think this flag was actually intended to throw even in production, and that the bug here is that it was after the __DEV__ check originally, where it should have been before. The rationale behind a throw here was that it is not a warning about something that by some heuristic probably has been done wrong. Instead, the user explicitly expressed that for this specifically computed it is wrong.

So I'd be supportive of moving this back to throw, but move the check outside DEV. My concern however is potentially breaking production systems, so I'm wondering if this should be a major change? Advocating for the devil, the spec says already it throws, so it is probably fine as patch.

mweststrate avatar Feb 01 '23 05:02 mweststrate

There are many warnings that should be fixed, like React key warnings, that are not errors to not deviate from production behavior, as otherwise bug reproductions can become extremely hard.

While it is true that lack of key in list is a React warning that should be fixed, but this mistake is generally very local, and can easily be caught during self-review and review, as well as via static linting.

However, it is not the case for things like enforceActions and requiresReaction. Errors on those are very remote, i.e. where they are required and where they happens are far from each other, many of the time not even in the same package. And with the help of proxy, some of the operations are implicit, making it even harder to recognize. So I don't think it's reasonable to put them together.

Also warnings don't always carry a stack (as in Firefox), and sometimes don't even carry the location (as in Safari), which can make it harder to locate the source of the issue given the remote natural of the issue.

If checking warnings is not part of the developers typical habits before putting up a PR, I'd indeed solve this as the infra level of your product, and raise on-screen alerts on any logged warning/error you believe must always be fixed in your company.

That is an interesting suggestion we will consider.

But as I mentioned above, other warnings are not generally an issue given their local nature in code, and we haven't been having problem with preventing them, so this issue is pretty much specific to the mobx ones.

upsuper avatar Feb 01 '23 05:02 upsuper

I would also argue that

raise on-screen alerts on any logged warning/error you believe must always be fixed in your company.

is not a very sustainable way to do it in long term.

You already don't consider behavior change switching from throwing to warning to be a major change, how can we ensure that the warning message wouldn't get accidentally reworded in a way we fail to capture it after a patch upgrade, and without even the change log mentioning it given it's trivial looking?

upsuper avatar Feb 01 '23 05:02 upsuper

You already don't consider behavior change switching from throwing to warning to be a major change

The change itself wasn't breaking, it only changed the dev experience. Obviously format of dev messages (not prod errors) is not something one should rely on, unless we would have designed it with such a goal in mind, because of a feedback like this (eg prefixing it with a code).

without even the change log mentioning it

https://github.com/mobxjs/mobx/releases/tag/mobx%406.3.3

urugator avatar Feb 01 '23 08:02 urugator