-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add computed.shallow annotation #2986
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: ba43d3f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Hi, thanks for the PR. Here is the reasoning why we're hesitant to provide this annotation. |
Just in case you're not aware, we do export shallow comparer, so you can do |
I don't quite agree with the reasoning there. It's been mentioned by the author of that issue as well that an O(n) comparison is not bad given that it already takes O(n) to generate the collection, and if a downstream is triggered, it will likely also take O(n) to handle the change. On the other hand, I believe the existence of |
I assume you mean "at least O(n)"? ... otherwise it takes O(n) (compare), to prevent O(n) (handle the change), so it never optimizes anything. Personally I would vote for deprecating |
Well not all O(n)s are equal. A shallow comparison should be pretty cheap, but there are more expensive O(n)s like if you rerender a list in a React component, which then triggers reconciliation. Technically it will still be O(n) if nothing changes, but it's a much more expensive O(n) than the comparison. |
Sure, just saying that the O(n) (handle the change) must be more expensive than comparison and if I follow correctly, you claim that this is usually the case, which I agree, but imo in practice it must be significantly more expensive to be effective.
Be careful so you're not simply replacing comparison of elements performed by react by a comparison of array items performed by mobx. There is no point in preventing react's reconciliation by doing own reconciliation, unless there is an expensive step in the middle (render function itself). |
React's reconciliation is... reconciliation, mobx's shallow comparison is not. Reconciliation isn't just a shallow comparison, it needs to compare constructor and props for each node as well on top of finding the match, because they are all new objects, and don't forget that the render function is generating all these new objects. Shallow comparison on the other hand can rely on object reference equality check which should be much cheaper. Also I would argue it's a pretty common case where changes to a collection almost always come with a size change (new item being added and removed rarely happen simultaneously), in those cases a shallow comparison on unequal case is O(1), and on equal case is a cheap O(n) to avoid more expensive process. |
@upsuper here you touch on an important point; the performance impact isn't just determined by the expensiveness of the shallow comparison, but also by the amount of 'cache hits'. I think for the vast majority of Take the following example:
It might be tempting to put a A
That being said I'm fine adding this, even for consistency. Would you mind expanding the docs in this PR that those decorators (similar reasoning applies to .struct) only add any benefits in specific cases, but not in general? |
Any progress on this PR? I think it's great!
Absolutely. It needs to be some kind of a reduction computation, not just returning the same data in a different shape. ....and I'm not so sure that's rare in practice. In our codebase at least, it happens all the time. We have lots of cases in our codebase of classes that contain observable mutable data that also expose computed properties that reveal structured reductions of that data (e.g., combos of max, min, filter, ...). The recomputation of those reductions is O(n), yes, but when most mutations don't change the reduced value and the comparator is able to detect that the reduced value hasn't changed, then it doesn't trigger further recomputations, and that's the win. Considering the example above with type HasCoords = { x: number, y: number };
/** A collection of things with coordinates. */
class ElementCollection<T extends HasCoords> {
@observable.ref
es: readonly T[] = [];
add(e: T) {
this.es.push(e);
}
/** The bounding box covering the elements. */
@computed.struct // <-- sensible use of .struct
get boundingBox(): { minX: number, maxX: number, minY: number, maxY: number } {
...
}
/** Elements that lie on a bounding-box boundary. */
@computed({ equals: comparer.shallow }) // <-- crying out for a computed.shallow shorthand!
get extrema(): readonly T[] {
const { minX, maxX, minY, maxY } = this.boundingBox;
return this.es.filter(e => e.x === minX || e.x === maxX || e.y === minY || e.y == maxY);
}
@computed // <-- this is something that doesn't make sense to .struct or .shallow on, as you point out
get sameDataDifferentShape() {
return { copy: [...this.es], json: JSON.stringify(this.es) };
}
} We use |
Sorry, have been focused on other priorities. Let me wrap up #3638 first, and check if it cleanly applies afterwards. In the mean time for anyone reading along, note that decorators are just higher order fns, so feel free to work around by defining your own utility should work: const computedShallow = computed({ equals: comparer.shallow })
// later
class X {
@computedShallow
get myComputation() { ...
} |
I think we could revisit this now that decorators have landed. Is there still a need / interest for this feature @upsuper? |
Sorry I have been quite busy lately, and almost forgot about this one. But yes, there is definitely still a need. I'll have a look into it tomorrow. |
@mweststrate I've rebased the branch to the latest and add some more documentation for |
In our (Canva) codebase, I've seen several places where
computed.shallow
would be useful, where we are currently using eithercomputed.struct
(unnecessarily) or a custom comparer function which is much longer to write. So I think it would be useful to add acomputed.shallow
parallel toobservable.shallow
.Code change checklist
/docs
. For new functionality, at leastAPI.md
should be updatednpm run perf
)