Skip to content

Commit d9d47ec

Browse files
committed
fix: Do not applyProps if value has not changed
1 parent 5e577ca commit d9d47ec

File tree

2 files changed

+62
-21
lines changed

2 files changed

+62
-21
lines changed

packages/lib/src/utils/compare.tsx

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
/**
32
* Simple deep equality check for two objects
43
* @param {Record<string, unknown>} a - The first object
@@ -16,23 +15,61 @@ export function deepEqual(a: Record<string, unknown>, b: Record<string, unknown>
1615
return aKeys.every(key => deepEqual(a[key] as Record<string, unknown>, b[key] as Record<string, unknown>));
1716
}
1817

19-
type Equalable = {
20-
equals: (other: unknown) => boolean;
21-
};
22-
23-
function hasEqualsMethod(obj: unknown): obj is Equalable {
24-
return typeof obj === 'object' &&
25-
obj !== null &&
26-
'equals' in obj &&
27-
typeof (obj as Equalable).equals === 'function';
28-
}
29-
30-
export const shallowEquals = (objA: Record<string, unknown>, objB: Record<string, unknown>): boolean => {
18+
interface Approximate {
19+
equalsApprox(other: unknown): boolean;
20+
}
21+
22+
interface Equatable {
23+
equals(other: unknown): boolean;
24+
}
25+
26+
/**
27+
* Check if two values are equal. Handles primitives, null/undefined,
28+
* and objects with `equalsApprox` or `equals` methods (Vec3, Color, etc.)
29+
*
30+
* Priority order:
31+
* 1. Strict equality (===)
32+
* 2. Floating point approximation (equalsApprox) - handles precision drift
33+
* 3. Structural equality (equals)
34+
*
35+
* @param a - First value to compare
36+
* @param b - Second value to compare
37+
* @returns True if values are equal, false otherwise
38+
*/
39+
export function valuesEqual(a: unknown, b: unknown): boolean {
40+
if (a === b) return true;
41+
42+
// Early exit if either is null/undefined (using type coercion)
43+
if (a == null || b == null) return false;
44+
45+
if (typeof a === 'object') {
46+
// Priority 1: Floating point approximation (handles precision drift)
47+
if ('equalsApprox' in a && typeof (a as Approximate).equalsApprox === 'function') {
48+
return (a as Approximate).equalsApprox(b);
49+
}
50+
51+
// Priority 2: Strict structural equality
52+
if ('equals' in a && typeof (a as Equatable).equals === 'function') {
53+
return (a as Equatable).equals(b);
54+
}
55+
}
56+
57+
// For other objects, return false to trigger re-apply (conservative)
58+
return false;
59+
}
60+
61+
/**
62+
* Shallow equality check for two objects. Compares each property using valuesEqual.
63+
*
64+
* @param objA - First object to compare
65+
* @param objB - Second object to compare
66+
* @returns True if objects are shallowly equal, false otherwise
67+
*/
68+
export const shallowEquals = (objA: Record<string, unknown>, objB: Record<string, unknown>): boolean => {
3169
// If the two objects are the same object, return true
3270
if (objA === objB) {
3371
return true;
3472
}
35-
3673

3774
// If either is not an object (null or primitives), return false
3875
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
@@ -48,15 +85,10 @@ type Equalable = {
4885
return false;
4986
}
5087

51-
// Check if all keys and their values are equal
88+
// Check if all keys and their values are equal using valuesEqual
5289
for (let i = 0; i < keysA.length; i++) {
5390
const key = keysA[i];
54-
const propA = objA[key];
55-
const propB = objB[key];
56-
// If the object has an equality operator, use this
57-
if(hasEqualsMethod(propA)) {
58-
return propA.equals(propB);
59-
} else if (propA !== propB) {
91+
if (!valuesEqual(objA[key], objB[key])) {
6092
return false;
6193
}
6294
}

packages/lib/src/utils/validation.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Color, Quat, Vec2, Vec3, Vec4, Mat4, Application, NullGraphicsDevice, M
22
import { getColorFromName } from "./color.ts";
33
import { Serializable } from "./types-utils.ts";
44
import { env } from "./env.ts";
5+
import { valuesEqual } from "./compare.tsx";
56

67
// Limit the size of the warned set to prevent memory leaks
78
const MAX_WARNED_SIZE = 1000;
@@ -218,6 +219,7 @@ export function validatePropsWithDefaults<T extends object, InstanceType>(
218219
* @param schema The schema of the container
219220
* @param props The props to apply
220221
*/
222+
221223
export function applyProps<T extends Record<string, unknown>, InstanceType>(
222224
instance: InstanceType,
223225
schema: Schema<T, InstanceType>,
@@ -227,6 +229,13 @@ export function applyProps<T extends Record<string, unknown>, InstanceType>(
227229
if (key in schema) {
228230
const propDef = schema[key as keyof T] as PropValidator<T[keyof T], InstanceType>;
229231
if (propDef) {
232+
const currentValue = (instance as Record<string, unknown>)[key];
233+
234+
// Skip if value hasn't changed (avoids side effects from setters)
235+
if (valuesEqual(currentValue, value)) {
236+
return;
237+
}
238+
230239
if (propDef.apply) {
231240
// Use type assertion to satisfy the type checker
232241
propDef.apply(instance, props, key as string);

0 commit comments

Comments
 (0)