Skip to content

Commit aed4ecf

Browse files
authored
fix(react-ecs): don't drop an optional prop set for the first time (#1422)
propsChanged diffed only the keys present in prevProps, so an optional prop that appears for the first time (absent from prevProps, present in nextProps) was silently dropped on its first change. Add a second loop over nextProps for keys not in prevProps. This is allocation-free (no Object.keys/Set) to keep this per-frame hot path cheap, and adds a propsChanged unit test covering the first-appearance case.
1 parent 401ee50 commit aed4ecf

3 files changed

Lines changed: 44 additions & 4 deletions

File tree

packages/@dcl/react-ecs/src/reconciler/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,25 @@ export function propsChanged<K extends keyof EntityComponents>(
3838
}
3939

4040
const changes: Partial<EntityComponents[K]> = {}
41+
// Iterate prevProps (this also catches removed keys, where nextProps[k] is undefined)...
4142
for (const k in prevProps) {
4243
const propKey = k as keyof typeof prevProps
4344
if (!isEqual(prevProps[propKey], nextProps[propKey])) {
4445
changes[propKey] = nextProps[propKey]
4546
}
4647
}
48+
// ...then catch keys present only in nextProps (an optional prop set for the first time,
49+
// e.g. an undefined value becoming defined). Iterating prevProps alone would drop these.
50+
// Two allocation-free loops are used instead of a Set union to keep this hot path cheap.
51+
// These keys aren't in prevProps, so emitting them whenever they're defined is both cheaper
52+
// than (and avoids the falsy-value trap of) an isEqual(undefined, ...) comparison.
53+
for (const k in nextProps) {
54+
if (k in prevProps) continue
55+
const propKey = k as keyof typeof nextProps
56+
if (nextProps[propKey] !== undefined) {
57+
changes[propKey] = nextProps[propKey]
58+
}
59+
}
4760

4861
if (!Object.keys(changes).length) {
4962
return
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { propsChanged } from '../../packages/@dcl/react-ecs/src/reconciler/utils'
2+
3+
describe('reconciler propsChanged', () => {
4+
it('detects a key present only in nextProps (optional prop set for the first time)', () => {
5+
// Regression: the diff used to iterate only the keys present in prevProps, so a field
6+
// appearing for the first time (absent from prevProps, present in nextProps) was silently
7+
// dropped on its first change. This is what made e.g. the first scroll-to-target — and any
8+
// optional prop going from undefined to a value — never reach the engine.
9+
expect(propsChanged('uiTransform', {}, { width: 100 })).toEqual({
10+
type: 'put',
11+
component: 'uiTransform',
12+
props: { width: 100 }
13+
})
14+
})
15+
16+
it('still detects a changed existing key', () => {
17+
expect(propsChanged('uiTransform', { width: 100 }, { width: 200 })).toEqual({
18+
type: 'put',
19+
component: 'uiTransform',
20+
props: { width: 200 }
21+
})
22+
})
23+
24+
it('returns undefined when nothing changed', () => {
25+
expect(propsChanged('uiTransform', { width: 100 }, { width: 100 })).toBeUndefined()
26+
})
27+
})

test/snapshots/production-bundles/ui.ts.crdt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,17 @@ CALL onUpdate(0)
5353
ALIVE_OBJS_DELTA ~= 0.31k
5454
CALL onUpdate(0.1)
5555
Scene: PUT_COMPONENT e=0x200 c=1 t=2 data={"position":{"x":8,"y":1,"z":8},"rotation":{"x":0,"y":0.008726535364985466,"z":0,"w":0.9999619126319885},"scale":{"x":1,"y":1,"z":1},"parent":0}
56-
OPCODES ~= 70k
56+
OPCODES ~= 76k
5757
MALLOC_COUNT = 259
5858
ALIVE_OBJS_DELTA ~= 0.13k
5959
CALL onUpdate(0.1)
6060
Scene: PUT_COMPONENT e=0x200 c=1 t=3 data={"position":{"x":8,"y":1,"z":8},"rotation":{"x":0,"y":0.017452405765652657,"z":0,"w":0.9998477101325989},"scale":{"x":1,"y":1,"z":1},"parent":0}
61-
OPCODES ~= 69k
61+
OPCODES ~= 74k
6262
MALLOC_COUNT = 35
6363
ALIVE_OBJS_DELTA ~= 0.02k
6464
CALL onUpdate(0.1)
6565
Scene: PUT_COMPONENT e=0x200 c=1 t=4 data={"position":{"x":8,"y":1,"z":8},"rotation":{"x":0,"y":0.026176948100328445,"z":0,"w":0.9996573328971863},"scale":{"x":1,"y":1,"z":1},"parent":0}
66-
OPCODES ~= 69k
66+
OPCODES ~= 74k
6767
MALLOC_COUNT = 0
6868
ALIVE_OBJS_DELTA ~= 0.00k
69-
MEMORY_USAGE_COUNT ~= 1889.04k bytes
69+
MEMORY_USAGE_COUNT ~= 1889.20k bytes

0 commit comments

Comments
 (0)