Skip to content

Commit 3a83d2f

Browse files
authored
fix: reconciler sending echoed value back for UiInput component (#1331)
* fix: reconciler sending echoed value back for UiInput component * update snapshots * change fix
1 parent b055846 commit 3a83d2f

File tree

3 files changed

+83
-4
lines changed

3 files changed

+83
-4
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export function createReconciler(
6060
// Store the onChange callbacks to be runned every time a Result has changed
6161
const changeEvents = new Map<Entity, Map<number, OnChangeState | undefined>>()
6262
const clickEvents = new Map<Entity, Map<PointerEventType, Callback>>()
63+
// Track the last value reported by the renderer for each input entity,
64+
// so we can avoid echoing it back and causing keystroke drops.
65+
const lastInputResultValues = new Map<Entity, string | undefined>()
6366
// Initialize components
6467
const UiTransform = components.UiTransform(engine)
6568
const UiText = components.UiText(engine)
@@ -175,6 +178,18 @@ export function createReconciler(
175178
delete (props as any).onSubmit
176179
}
177180

181+
// Prevent keystroke drops: when React echoes back the same value the renderer
182+
// reported, strip it from the props so the component isn't marked dirty for it.
183+
// This avoids sending a stale value that overwrites what the user is currently typing.
184+
if (
185+
componentName === 'uiInput' &&
186+
'value' in props &&
187+
lastInputResultValues.has(instance.entity) &&
188+
(props as any).value === lastInputResultValues.get(instance.entity)
189+
) {
190+
delete (props as any).value
191+
}
192+
178193
// We check if there is any key pending to be changed to avoid updating the existing component
179194
if (!Object.keys(props).length) {
180195
return
@@ -192,6 +207,7 @@ export function createReconciler(
192207
function removeChildEntity(instance: Instance) {
193208
changeEvents.delete(instance.entity)
194209
clickEvents.delete(instance.entity)
210+
lastInputResultValues.delete(instance.entity)
195211
engine.removeEntity(instance.entity)
196212
for (const child of instance._child) {
197213
removeChildEntity(child)
@@ -252,6 +268,9 @@ export function createReconciler(
252268
const resultComponentId =
253269
componentId === UiDropdown.componentId ? UiDropdownResult.componentId : UiInputResult.componentId
254270
engine.getComponent<PBUiInputResult | PBUiDropdownResult>(resultComponentId).onChange(entity, (value) => {
271+
if (resultComponentId === UiInputResult.componentId) {
272+
lastInputResultValues.set(entity, value?.value as string | undefined)
273+
}
255274
if ((value as PBUiInputResult)?.isSubmit) {
256275
const onSubmit = changeEvents.get(entity)?.get(componentId)?.onSubmitCallback
257276
onSubmit && onSubmit(value?.value)

test/react-ecs/input.spec.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,66 @@ import { ReactEcs, Input } from '../../packages/@dcl/react-ecs/src'
44
import { Color4 } from '../../packages/@dcl/sdk/math'
55
import { setupEngine } from './utils'
66

7+
describe('Ui Input - smooth typing', () => {
8+
it('should not echo back the same value the renderer reported, preventing keystroke drops', async () => {
9+
const { engine, uiRenderer } = setupEngine()
10+
const UiInput = components.UiInput(engine)
11+
const UiInputResult = components.UiInputResult(engine)
12+
const uiEntity = ((engine.addEntity() as number) + 1) as Entity
13+
14+
let inputValue = ''
15+
const ui = () => (
16+
<Input
17+
uiTransform={{ width: 100 }}
18+
value={inputValue}
19+
onChange={(val) => { inputValue = val ?? '' }}
20+
placeholder="type here"
21+
/>
22+
)
23+
uiRenderer.setUiRenderer(ui)
24+
await engine.update(1)
25+
26+
// Simulate the renderer reporting that the user typed "hello"
27+
UiInputResult.create(uiEntity, { value: 'hello' })
28+
await engine.update(1)
29+
expect(inputValue).toBe('hello')
30+
31+
// React re-renders with value="hello" (echoing it back).
32+
// The reconciler should strip the echoed value from the update,
33+
// so the component's value on the ECS side stays unchanged (not overwritten).
34+
const valueBefore = UiInput.get(uiEntity).value
35+
await engine.update(1)
36+
expect(UiInput.get(uiEntity).value).toBe(valueBefore)
37+
})
38+
39+
it('should still allow programmatic value changes that differ from renderer state', async () => {
40+
const { engine, uiRenderer } = setupEngine()
41+
const UiInput = components.UiInput(engine)
42+
const UiInputResult = components.UiInputResult(engine)
43+
const uiEntity = ((engine.addEntity() as number) + 1) as Entity
44+
45+
let inputValue = ''
46+
const ui = () => (
47+
<Input
48+
uiTransform={{ width: 100 }}
49+
value={inputValue}
50+
onChange={(val) => { inputValue = val ?? '' }}
51+
placeholder="type here"
52+
/>
53+
)
54+
uiRenderer.setUiRenderer(ui)
55+
await engine.update(1)
56+
57+
UiInputResult.create(uiEntity, { value: 'hello' })
58+
await engine.update(1)
59+
expect(inputValue).toBe('hello')
60+
61+
inputValue = ''
62+
await engine.update(1)
63+
expect(UiInput.get(uiEntity).value).toBe('')
64+
})
65+
})
66+
767
describe('Ui Listeners React Ecs', () => {
868
it('should run onChange if it was a keyboard event', async () => {
969
const { engine, uiRenderer } = setupEngine()

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SCENE_COMPILED_JS_SIZE_PROD=379.9k bytes
1+
SCENE_COMPILED_JS_SIZE_PROD=380.1k bytes
22
(start empty vm 0.21.0-3680274614.commit-1808aa1)
33
OPCODES ~= 0k
44
MALLOC_COUNT = 1005
@@ -9,7 +9,7 @@ EVAL test/snapshots/production-bundles/ui.js
99
REQUIRE: ~system/EngineApi
1010
REQUIRE: ~system/EngineApi
1111
OPCODES ~= 76k
12-
MALLOC_COUNT = 20933
12+
MALLOC_COUNT = 20938
1313
ALIVE_OBJS_DELTA ~= 4.21k
1414
CALL onStart()
1515
OPCODES ~= 0k
@@ -48,7 +48,7 @@ CALL onUpdate(0)
4848
Scene: PUT_COMPONENT e=0x209 c=1093 t=1 data={"placeholder":"","disabled":false}
4949
Scene: PUT_COMPONENT e=0x201 c=1094 t=1 data={"acceptEmpty":false,"options":["BOEDO","CASLA"],"selectedIndex":0,"disabled":false,"color":{"r":1,"g":0,"b":0,"a":1}}
5050
OPCODES ~= 143k
51-
MALLOC_COUNT = 715
51+
MALLOC_COUNT = 718
5252
ALIVE_OBJS_DELTA ~= 0.25k
5353
CALL onUpdate(0.1)
5454
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}
@@ -65,4 +65,4 @@ CALL onUpdate(0.1)
6565
OPCODES ~= 67k
6666
MALLOC_COUNT = 0
6767
ALIVE_OBJS_DELTA ~= 0.00k
68-
MEMORY_USAGE_COUNT ~= 1790.79k bytes
68+
MEMORY_USAGE_COUNT ~= 1791.46k bytes

0 commit comments

Comments
 (0)