Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/healthy-maps-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@lynx-js/react": patch
---

Fix "TypeError: cannot read property '0' of undefined" in deferred list-item scenarios.

Deferred `componentAtIndex` causes nodes that quickly appear/disappear to be enqueued without `__elements`. Update `signMap` before `__FlushElementTre` to resolve the issue.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
54 changes: 54 additions & 0 deletions packages/react/runtime/__test__/list.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3032,6 +3032,60 @@ describe('list componentAtIndexes', () => {
]
`);
});

it('should update signMap before __FlushElementTree', () => {
const b = new SnapshotInstance(s0);
b.ensureElements();
const listRef = b.__elements[3];
const d0 = new SnapshotInstance(s1);
const d1 = new SnapshotInstance(s1);
const d2 = new SnapshotInstance(s1);
d0.setAttribute(0, { 'item-key': 'list-item-0' });
d1.setAttribute(0, { 'item-key': 'list-item-1' });
d2.setAttribute(0, { 'item-key': 'list-item-2' });
b.insertBefore(d0);
b.insertBefore(d1);
b.insertBefore(d2);
__pendingListUpdates.flush();

const listID = __GetElementUniqueID(listRef);
const signMap = gSignMap[listID];
const recycleMap = gRecycleMap[listID];

{
const flushElementTreeSpy = vi.spyOn(globalThis, '__FlushElementTree');
const signMapSetSpy = vi.spyOn(signMap, 'set');

const component = [];
component[0] = elementTree.triggerComponentAtIndex(listRef, 0);
component[1] = elementTree.triggerComponentAtIndex(listRef, 1);
elementTree.triggerEnqueueComponent(listRef, component[0]);
elementTree.triggerEnqueueComponent(listRef, component[1]);
// no reuse occurs
expect(signMapSetSpy).toHaveBeenCalled();
expect(flushElementTreeSpy).toHaveBeenCalled();
expect(signMapSetSpy.mock.invocationCallOrder[0])
.toBeLessThan(flushElementTreeSpy.mock.invocationCallOrder[0]);

flushElementTreeSpy.mockRestore();
signMapSetSpy.mockRestore();
}

// re-spy
const flushElementTreeSpy = vi.spyOn(globalThis, '__FlushElementTree');
const signMapSetSpy = vi.spyOn(signMap, 'set');
const recycleSignMap = recycleMap.get(s1);
expect(Array.from(recycleSignMap.keys()).length).toBe(2);
// reuse occurs
elementTree.triggerComponentAtIndex(listRef, 2);
expect(signMapSetSpy).toHaveBeenCalled();
expect(flushElementTreeSpy).toHaveBeenCalled();
expect(signMapSetSpy.mock.invocationCallOrder[0])
.toBeLessThan(flushElementTreeSpy.mock.invocationCallOrder[0]);

flushElementTreeSpy.mockRestore();
signMapSetSpy.mockRestore();
});
});

describe('list-item with "defer" attribute', () => {
Expand Down
8 changes: 6 additions & 2 deletions packages/react/runtime/src/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ export function componentAtIndexFactory(
}
const root = childCtx.__element_root!;
applyRefQueue();
// In the defer `list-item` scenario, `componentAtIndex` occurs with delay.
// Within `componentAtIndex`, nodes that quickly appear and disappear due to re-layout will be enqueued again,
// causing the mapping relationship between sign and SnapshotInstance to become corrupted.
// This results in a SnapshotInstance without `__elements` being enqueued.
signMap.set(sign, childCtx);
if (!enableBatchRender) {
const flushOptions: FlushOptions = {
triggerLayout: true,
Expand All @@ -181,7 +186,6 @@ export function componentAtIndexFactory(
}
__FlushElementTree(root, flushOptions);
}
signMap.set(sign, childCtx);
return sign;
}

Expand All @@ -190,6 +194,7 @@ export function componentAtIndexFactory(
__AppendElement(list, root);
const sign = __GetElementUniqueID(root);
applyRefQueue();
signMap.set(sign, childCtx);
if (!enableBatchRender) {
__FlushElementTree(root, {
triggerLayout: true,
Expand All @@ -202,7 +207,6 @@ export function componentAtIndexFactory(
asyncFlush: true,
});
}
signMap.set(sign, childCtx);
return sign;
};

Expand Down
Loading