-
Notifications
You must be signed in to change notification settings - Fork 33
fix: Do not applyProps if value has not changed
#298
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
base: main
Are you sure you want to change the base?
Conversation
|
commit: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces an optimization to prevent unnecessary prop applications that trigger setter side effects, specifically addressing issue #297 where the GSplat component's unified flag couldn't be changed after the component was enabled.
- Adds a
valuesEqualutility function that handles equality checking for primitives and PlayCanvas objects (Vec2, Vec3, Color, etc.) with support for approximate floating-point comparisons - Modifies
applyPropsto skip property application when values haven't changed, avoiding unwanted side effects from setters - Refactors
shallowEqualsto use the newvaluesEqualfunction for more robust property comparisons
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/lib/src/utils/compare.tsx | Introduces the new valuesEqual function with support for PlayCanvas object equality checking via equalsApprox and equals methods; updates shallowEquals to leverage this new function |
| packages/lib/src/utils/validation.ts | Integrates valuesEqual into applyProps to check if property values have changed before applying them, preventing unnecessary setter invocations |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const currentValue = (instance as Record<string, unknown>)[key]; | ||
|
|
||
| // Skip if value hasn't changed (avoids side effects from setters) | ||
| if (valuesEqual(currentValue, value)) { | ||
| return; | ||
| } |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The equality check compares the current instance property value with the new prop value, but this comparison may be incorrect when a custom apply function exists. The apply function often transforms the input (e.g., an array [1, 2, 3] gets transformed into a Vec3 object). This means you're comparing a Vec3 object with an array, which will always return false and the optimization won't work.
Consider checking equality after the transformation, or store the last applied raw value to compare against the new raw value before transformation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that is a good suggestion, will try to rework this
| export function valuesEqual(a: unknown, b: unknown): boolean { | ||
| if (a === b) return true; | ||
|
|
||
| // Early exit if either is null/undefined (using type coercion) | ||
| if (a == null || b == null) return false; | ||
|
|
||
| if (typeof a === 'object') { | ||
| // Priority 1: Floating point approximation (handles precision drift) | ||
| if ('equalsApprox' in a && typeof (a as Approximate).equalsApprox === 'function') { | ||
| return (a as Approximate).equalsApprox(b); | ||
| } | ||
|
|
||
| // Priority 2: Strict structural equality | ||
| if ('equals' in a && typeof (a as Equatable).equals === 'function') { | ||
| return (a as Equatable).equals(b); | ||
| } | ||
| } | ||
|
|
||
| // For other objects, return false to trigger re-apply (conservative) | ||
| return false; | ||
| } |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new valuesEqual function lacks test coverage. Given the critical nature of this function (it's used to prevent unnecessary prop applications and avoid side effects), it should have comprehensive tests covering:
- Primitive comparisons (numbers, strings, booleans, null, undefined)
- Object comparisons using equalsApprox (Vec2, Vec3, Vec4, Color, Quat)
- Object comparisons using equals
- Edge cases like comparing objects with arrays
Consider adding a test file similar to picker.test.tsx to ensure this function behaves correctly in all scenarios.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion
| equalsApprox(other: unknown): boolean; | ||
| } | ||
|
|
||
| interface Equatable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think Equatable is quite right... Exact maybe?
What has changed?
Certain setters actually have side effects (especially in the
GSplatComponent) and we want to avoid triggering these side effects if the value has not changed. Examples:This can easily happen whenever we need to re-render a GSplat component and all the props get re-applied again. This can cause undesired effects.