From 11c7828fa7c44b871d8c77dfe87edbb4d46ffe44 Mon Sep 17 00:00:00 2001 From: mixelburg <52622705+mixelburg@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:06:30 +0200 Subject: [PATCH 1/2] fix(jsx/dom): handle empty arrays in render children loop (#4729) * fix(jsx/dom): handle empty arrays in render children loop When an empty array is spliced into the children list, the loop index still increments, causing the next child to be skipped. This leads to a TypeError when accessing properties of undefined nodes. Add i-- and continue after splice so the loop re-examines the same index after flattening, correctly handling empty arrays. * ci: apply automated fixes --------- Co-authored-by: Maks Pikov Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/jsx/dom/index.test.tsx | 18 ++++++++++++++++++ src/jsx/dom/render.ts | 2 ++ 2 files changed, 20 insertions(+) diff --git a/src/jsx/dom/index.test.tsx b/src/jsx/dom/index.test.tsx index 2952852bc..9e1e2bbfc 100644 --- a/src/jsx/dom/index.test.tsx +++ b/src/jsx/dom/index.test.tsx @@ -119,6 +119,24 @@ describe('DOM', () => { expect(root.innerHTML).toBe('Hello') }) + it('render with empty array followed by non-empty array', () => { + const tags: string[] = [] + const terms: string[] = ['hello'] + const App = () => ( +
+ {tags.map((x) => ( + {x} + ))} + {terms.map((x) => ( + {x} + ))} + +
+ ) + render(, root) + expect(root.innerHTML).toBe('
hello
') + }) + describe('performance', () => { it('should be O(N) for each additional element', () => { const App = () => ( diff --git a/src/jsx/dom/render.ts b/src/jsx/dom/render.ts index 145b6ac31..b6311f703 100644 --- a/src/jsx/dom/render.ts +++ b/src/jsx/dom/render.ts @@ -487,6 +487,8 @@ export const build = (context: Context, node: NodeObject, children?: Child[]): v for (let i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { children.splice(i, 1, ...(children[i] as Child[]).flat()) + i-- + continue } let child = buildNode(children[i]) if (child) { From 11a19424497a0e4610757c4e6900731a073dd938 Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Thu, 19 Feb 2026 11:04:00 +0900 Subject: [PATCH 2/2] perf(jsx/dom): flatten children once at the start to avoid repeated flattening (#4730) * perf(jsx/dom): flatten children once at the start to avoid repeated flattening * test(jsx/dom): Move the test to the correct position location and add a regression test --- src/jsx/dom/index.test.tsx | 91 ++++++++++++++++++++++++++++++-------- src/jsx/dom/render.ts | 2 +- 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/src/jsx/dom/index.test.tsx b/src/jsx/dom/index.test.tsx index 9e1e2bbfc..ca3b86553 100644 --- a/src/jsx/dom/index.test.tsx +++ b/src/jsx/dom/index.test.tsx @@ -119,24 +119,6 @@ describe('DOM', () => { expect(root.innerHTML).toBe('Hello') }) - it('render with empty array followed by non-empty array', () => { - const tags: string[] = [] - const terms: string[] = ['hello'] - const App = () => ( -
- {tags.map((x) => ( - {x} - ))} - {terms.map((x) => ( - {x} - ))} - -
- ) - render(, root) - expect(root.innerHTML).toBe('
hello
') - }) - describe('performance', () => { it('should be O(N) for each additional element', () => { const App = () => ( @@ -675,6 +657,79 @@ describe('DOM', () => { expect(root.innerHTML).toBe('
12
') }) + it('empty array and non-empty array', async () => { + const App = () => ( +
+ {[]} + {[1]} +
+ ) + render(, root) + expect(root.innerHTML).toBe('
1
') + }) + + it('nested array', async () => { + const nestedChildren: Child = [[[1], 2]] + const App = () =>
{nestedChildren}
+ render(, root) + expect(root.innerHTML).toBe('
12
') + }) + + it('sparse array with nested child', async () => { + const sparseChildren: Child[] = [] + sparseChildren[1] = [1] + const App = () =>
{sparseChildren}
+ render(, root) + expect(root.innerHTML).toBe('
1
') + }) + + it('toggle empty array and non-empty array on update', async () => { + let setVisible: (value: boolean) => void = () => {} + const App = () => { + const [visible, _setVisible] = useState(false) + setVisible = _setVisible + return ( +
+ {visible ? [] : [A]} + {visible ? [B] : []} +
+ ) + } + render(, root) + expect(root.innerHTML).toBe('
A
') + + setVisible(true) + await Promise.resolve() + expect(root.innerHTML).toBe('
B
') + + setVisible(false) + await Promise.resolve() + expect(root.innerHTML).toBe('
A
') + }) + + it('reshape nested array on update', async () => { + let setPattern: (value: number) => void = () => {} + const App = () => { + const [pattern, _setPattern] = useState(0) + setPattern = _setPattern + const children: Child = + pattern === 0 + ? [[A], B] + : [A, [B]] + return
{children}
+ } + render(, root) + expect(root.innerHTML).toBe('
AB
') + + setPattern(1) + await Promise.resolve() + expect(root.innerHTML).toBe('
AB
') + + setPattern(0) + await Promise.resolve() + expect(root.innerHTML).toBe('
AB
') + }) + it('use the same children multiple times', async () => { const MultiChildren = ({ children }: { children: Child }) => ( <> diff --git a/src/jsx/dom/render.ts b/src/jsx/dom/render.ts index b6311f703..5d2eae62e 100644 --- a/src/jsx/dom/render.ts +++ b/src/jsx/dom/render.ts @@ -486,7 +486,7 @@ export const build = (context: Context, node: NodeObject, children?: Child[]): v let prevNode: Node | undefined for (let i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { - children.splice(i, 1, ...(children[i] as Child[]).flat()) + children.splice(i, 1, ...((children[i] as unknown[]).flat(Infinity) as Child[])) i-- continue }