From 249a5995918aa5e17e1f7f4108d7843b8bdf7f88 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 24 Oct 2025 17:33:37 +0200 Subject: [PATCH 1/2] Experiment: add moveBefore rather than insertBefor when available --- src/constants.js | 3 + src/diff/children.js | 19 +- test/_util/logCall.js | 8 + test/browser/focus.test.jsx | 116 +-- test/browser/fragments.test.jsx | 774 ++++++++++-------- test/browser/keys.test.jsx | 215 ++--- .../lifecycles/shouldComponentUpdate.test.jsx | 4 + test/browser/render.test.jsx | 58 +- 8 files changed, 657 insertions(+), 540 deletions(-) diff --git a/src/constants.js b/src/constants.js index 1ad82b208e..194e88231a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -33,3 +33,6 @@ export const EMPTY_OBJ = /** @type {any} */ ({}); export const EMPTY_ARR = []; export const MATHML_TOKEN_ELEMENTS = /(mi|mn|mo|ms$|mte|msp)/; + +export const HAS_MOVE_BEFORE_SUPPORT = + typeof window !== 'undefined' && 'moveBefore' in Element.prototype; diff --git a/src/diff/children.js b/src/diff/children.js index 42cd334828..b169f54ef6 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -6,7 +6,8 @@ import { INSERT_VNODE, MATCHED, UNDEFINED, - NULL + NULL, + HAS_MOVE_BEFORE_SUPPORT } from '../constants'; import { isArray } from '../util'; import { getDomSibling } from '../component'; @@ -129,8 +130,9 @@ export function diffChildren( } let shouldPlace = childVNode._flags & INSERT_VNODE; + const isMounting = oldVNode == NULL || oldVNode._original == NULL; if (shouldPlace || oldVNode._children === childVNode._children) { - oldDom = insert(childVNode, oldDom, parentDom, shouldPlace); + oldDom = insert(childVNode, oldDom, parentDom, shouldPlace, isMounting); } else if (typeof childVNode.type == 'function' && result !== UNDEFINED) { oldDom = result; } else if (newDom) { @@ -339,9 +341,10 @@ function constructNewChildrenArray( * @param {PreactElement} oldDom * @param {PreactElement} parentDom * @param {number} shouldPlace + * @param {boolean} isMounting * @returns {PreactElement} */ -function insert(parentVNode, oldDom, parentDom, shouldPlace) { +function insert(parentVNode, oldDom, parentDom, shouldPlace, isMounting) { // Note: VNodes in nested suspended trees may be missing _children. if (typeof parentVNode.type == 'function') { let children = parentVNode._children; @@ -352,7 +355,7 @@ function insert(parentVNode, oldDom, parentDom, shouldPlace) { // children's _parent pointer to point to the newVNode (parentVNode // here). children[i]._parent = parentVNode; - oldDom = insert(children[i], oldDom, parentDom, shouldPlace); + oldDom = insert(children[i], oldDom, parentDom, shouldPlace, false); } } @@ -362,7 +365,13 @@ function insert(parentVNode, oldDom, parentDom, shouldPlace) { if (oldDom && parentVNode.type && !oldDom.parentNode) { oldDom = getDomSibling(parentVNode); } - parentDom.insertBefore(parentVNode._dom, oldDom || NULL); + + if (HAS_MOVE_BEFORE_SUPPORT && !isMounting) { + // @ts-expect-error This isn't added to TypeScript lib.d.ts yet + parentDom.moveBefore(parentVNode._dom, oldDom); + } else { + parentDom.insertBefore(parentVNode._dom, oldDom || NULL); + } } oldDom = parentVNode._dom; } diff --git a/test/_util/logCall.js b/test/_util/logCall.js index 3fb094ca78..64fd3141b9 100644 --- a/test/_util/logCall.js +++ b/test/_util/logCall.js @@ -39,6 +39,14 @@ export function logCall(obj, method) { } break; } + case 'moveBefore': { + if (args[1] === null && args.length === 2) { + operation = `${serialize(this)}.appendChild(${serialize(args[0])})`; + } else { + operation = `${serialize(this)}.insertBefore(${c})`; + } + break; + } default: { operation = `${serialize(this)}.${String(method)}(${c})`; break; diff --git a/test/browser/focus.test.jsx b/test/browser/focus.test.jsx index 23656a0d86..6dcc31a64e 100644 --- a/test/browser/focus.test.jsx +++ b/test/browser/focus.test.jsx @@ -5,6 +5,9 @@ import { div, span, input as inputStr, h1, h2 } from '../_util/dom'; /* eslint-disable react/jsx-boolean-value */ +const supportsMove = 'moveBefore' in Element.prototype; +console.log('SUPPORTS MOVE BEFORE:', supportsMove); + describe('focus', () => { /** @type {HTMLDivElement} */ let scratch; @@ -155,28 +158,33 @@ describe('focus', () => { teardown(scratch); }); - it.skip('should maintain focus when swapping elements', () => { - render( - - - fooo - , - scratch - ); + (supportsMove ? it : it.skip)( + 'should maintain focus when swapping elements', + () => { + render( + + + fooo + , + scratch + ); - const input = focusInput(); - expect(scratch.innerHTML).to.equal(getListHtml([], ['fooo'])); + const input = focusInput(); + expect(scratch.innerHTML).to.equal(getListHtml([], ['fooo'])); - render( - - fooo - - , - scratch - ); - validateFocus(input); - expect(scratch.innerHTML).to.equal(getListHtml(['fooo'], [])); - }); + render( + + fooo + + , + scratch + ); + + expect(scratch.innerHTML).to.equal(getListHtml(['fooo'], [])); + // TODO: looks like currently the selection isn't retained when moving + validateFocus(input); + } + ); it('should maintain focus when moving the input around', () => { function App({ showFirst, showLast }) { @@ -470,23 +478,25 @@ describe('focus', () => { return (

Heading

- {!this.state.active - ? - foobar - - Hello World -

yo

-
- (input = i)} /> + {!this.state.active ? ( + + foobar + + Hello World +

yo

+
+ (input = i)} /> +
+ ) : ( + + + Hello World +

yo

- : - - Hello World -

yo

-
- foobar - (input = i)} /> -
} + foobar + (input = i)} /> +
+ )}
); } @@ -524,23 +534,25 @@ describe('focus', () => { return (

Heading

- {!this.state.active - ? - foobar - - Hello World -

yo

-
- (input = i)} value="foobar" /> + {!this.state.active ? ( + + foobar + + Hello World +

yo

+
+ (input = i)} value="foobar" /> +
+ ) : ( + + + Hello World +

yo

- : - - Hello World -

yo

-
- foobar - (input = i)} value="foobar" /> -
} + foobar + (input = i)} value="foobar" /> +
+ )}
); } diff --git a/test/browser/fragments.test.jsx b/test/browser/fragments.test.jsx index 946303be8e..0f8cd6ed1d 100644 --- a/test/browser/fragments.test.jsx +++ b/test/browser/fragments.test.jsx @@ -38,9 +38,12 @@ describe('Fragment', () => { let resetAppendChild; let resetRemove; let resetRemoveText; + let resetMoveBefore; beforeAll(() => { resetInsertBefore = logCall(Element.prototype, 'insertBefore'); + // @ts-expect-error + resetMoveBefore = logCall(Element.prototype, 'moveBefore'); resetAppendChild = logCall(Element.prototype, 'appendChild'); resetRemove = logCall(Element.prototype, 'remove'); resetRemoveText = logCall(Text.prototype, 'remove'); @@ -57,6 +60,7 @@ describe('Fragment', () => { }); afterAll(() => { + resetMoveBefore(); resetInsertBefore(); resetAppendChild(); resetRemove(); @@ -251,12 +255,14 @@ describe('Fragment', () => { it('should preserve state of children with 1 level nesting', () => { function Foo({ condition }) { - return condition - ? - : - -
World
-
; + return condition ? ( + + ) : ( + + +
World
+
+ ); } render(, scratch); @@ -273,13 +279,15 @@ describe('Fragment', () => { it('should preserve state between top-level fragments', () => { function Foo({ condition }) { - return condition - ? - - - : - - ; + return condition ? ( + + + + ) : ( + + + + ); } render(, scratch); @@ -301,22 +309,24 @@ describe('Fragment', () => { it('should preserve state of children nested at same level', () => { function Foo({ condition }) { - return condition - ? + return condition ? ( + + - - - + - : + + ) : ( + + - -
- - +
+ - ; + + + ); } render(, scratch); @@ -338,15 +348,17 @@ describe('Fragment', () => { it('should not preserve state in non-top-level fragment nesting', () => { function Foo({ condition }) { - return condition - ? - - - - - : + return condition ? ( + + - ; + + + ) : ( + + + + ); } render(, scratch); @@ -377,13 +389,15 @@ describe('Fragment', () => { it('should not preserve state of children if nested 2 levels without siblings', () => { function Foo({ condition }) { - return condition - ? - : - - - - ; + return condition ? ( + + ) : ( + + + + + + ); } render(, scratch); @@ -429,14 +443,16 @@ describe('Fragment', () => { it('should not preserve state of children if nested 2 levels with siblings', () => { function Foo({ condition }) { - return condition - ? - : - - - -
- ; + return condition ? ( + + ) : ( + + + + +
+ + ); } render(, scratch); @@ -463,11 +479,13 @@ describe('Fragment', () => { // and the state of Stateful is preserved function Foo({ condition }) { - return condition - ? - - - : {[]}; + return condition ? ( + + + + ) : ( + {[]} + ); } render(, scratch); @@ -484,11 +502,13 @@ describe('Fragment', () => { it('should preserve state between top level fragment and array', () => { function Foo({ condition }) { - return condition - ? [] - : - - ; + return condition ? ( + [] + ) : ( + + + + ); } render(, scratch); @@ -516,13 +536,15 @@ describe('Fragment', () => { // separate the two Stateful VNodes into different trees and state is not preserved between them. function Foo({ condition }) { - return condition - ? {[]} - : - - - - ; + return condition ? ( + {[]} + ) : ( + + + + + + ); } render(, scratch); @@ -539,9 +561,11 @@ describe('Fragment', () => { it('should not preserve state between array nested in fragment and double nested array', () => { function Foo({ condition }) { - return condition - ? {[]} - : [[]]; + return condition ? ( + {[]} + ) : ( + [[]] + ); } render(, scratch); @@ -558,13 +582,15 @@ describe('Fragment', () => { it('should preserve state between double nested fragment and double nested array', () => { function Foo({ condition }) { - return condition - ? - - - + return condition ? ( + + + - : [[]]; + + ) : ( + [[]] + ); } render(, scratch); @@ -581,14 +607,16 @@ describe('Fragment', () => { it('should not preserve state of children when the keys are different', () => { function Foo({ condition }) { - return condition - ? - - - : - - World - ; + return condition ? ( + + + + ) : ( + + + World + + ); } render(, scratch); @@ -605,13 +633,15 @@ describe('Fragment', () => { it('should not preserve state between unkeyed and keyed fragment', () => { function Foo({ condition }) { - return condition - ? - - - : - - ; + return condition ? ( + + + + ) : ( + + + + ); } // React & Preact: has the same behavior for components @@ -734,25 +764,27 @@ describe('Fragment', () => { it('should preserve state with reordering in multiple levels', () => { function Foo({ condition }) { - return condition - ?
- -
foo
-
- -
-
-
boop
-
- :
-
beep
- -
- -
-
bar
-
-
; + return condition ? ( +
+ +
foo
+
+ +
+
+
boop
+
+ ) : ( +
+
beep
+ +
+ +
+
bar
+
+
+ ); } const htmlForTrue = div([div('foo'), div(div('Hello')), div('boop')]); @@ -794,20 +826,22 @@ describe('Fragment', () => { it('should not preserve state when switching between a keyed fragment and an array', () => { function Foo({ condition }) { - return condition - ?
- { - - 1 - - - } - 2 -
- :
- {[1, ]} - 2 -
; + return condition ? ( +
+ { + + 1 + + + } + 2 +
+ ) : ( +
+ {[1, ]} + 2 +
+ ); } const html = div([span('1'), div('Hello'), span('2')]); @@ -1079,23 +1113,25 @@ describe('Fragment', () => { return (

Heading

- {!this.state.active - ? - foobar - - Hello World -

yo

-
- + {!this.state.active ? ( + + foobar + + Hello World +

yo

+
+ +
+ ) : ( + + + Hello World +

yo

- : - - Hello World -

yo

-
- foobar - -
} + foobar + +
+ )}
); } @@ -1191,12 +1227,14 @@ describe('Fragment', () => { const Foo = ({ condition }) => (
  1. 0
  2. - {condition - ? -
  3. 1
  4. -
  5. 2
  6. -
    - : [
  7. 1
  8. ,
  9. 2
  10. ]} + {condition ? ( + +
  11. 1
  12. +
  13. 2
  14. +
    + ) : ( + [
  15. 1
  16. ,
  17. 2
  18. ] + )}
  19. 3
); @@ -1236,12 +1274,12 @@ describe('Fragment', () => { const Foo = ({ condition }) => (
  1. 0
  2. - {condition - ? -
  3. 1
  4. -
  5. 2
  6. -
    - : null} + {condition ? ( + +
  7. 1
  8. +
  9. 2
  10. +
    + ) : null}
  11. 3
  12. 4
@@ -1374,20 +1412,20 @@ describe('Fragment', () => { it('should support conditional beginning and end Fragments', () => { const Foo = ({ condition }) => (
    - {condition - ? -
  1. 0
  2. -
  3. 1
  4. -
    - : null} + {condition ? ( + +
  5. 0
  6. +
  7. 1
  8. +
    + ) : null}
  9. 2
  10. 2
  11. - {condition - ? null - : -
  12. 3
  13. -
  14. 4
  15. -
    } + {condition ? null : ( + +
  16. 3
  17. +
  18. 4
  19. +
    + )}
); @@ -1437,28 +1475,28 @@ describe('Fragment', () => { it('should support nested conditional beginning and end Fragments', () => { const Foo = ({ condition }) => (
    - {condition - ? + {condition ? ( + + - -
  1. 0
  2. -
  3. 1
  4. -
    +
  5. 0
  6. +
  7. 1
  8. - : null} +
    + ) : null}
  9. 2
  10. 3
  11. - {condition - ? null - : + {condition ? null : ( + + - -
  12. 4
  13. -
  14. 5
  15. -
    +
  16. 4
  17. +
  18. 5
  19. -
    } +
    +
    + )}
); @@ -1509,26 +1547,28 @@ describe('Fragment', () => { // Also fails if the # of divs outside the Fragment equals or exceeds // the # inside the Fragment for both conditions function Foo({ condition }) { - return condition - ?
- -
foo
-
- -
-
-
boop
-
boop
-
- :
-
beep
- -
- -
-
bar
-
-
; + return condition ? ( +
+ +
foo
+
+ +
+
+
boop
+
boop
+
+ ) : ( +
+
beep
+ +
+ +
+
bar
+
+
+ ); } const htmlForTrue = div([ @@ -1584,29 +1624,31 @@ describe('Fragment', () => { // Also fails if the # of divs outside the Fragment equals or exceeds // the # inside the Fragment for both conditions function Foo({ condition }) { - return condition - ?
- -
foo
-
- -
-
-
boop
-
boop
-
boop
-
- :
-
beep
-
beep
-
beep
- -
- -
-
bar
-
-
; + return condition ? ( +
+ +
foo
+
+ +
+
+
boop
+
boop
+
boop
+
+ ) : ( +
+
beep
+
beep
+
beep
+ +
+ +
+
bar
+
+
+ ); } const htmlForTrue = div([ @@ -1719,15 +1761,17 @@ describe('Fragment', () => { it('should render components that conditionally return Fragments', () => { const Foo = ({ condition }) => - condition - ? -
1
-
2
-
- :
-
3
-
4
-
; + condition ? ( + +
1
+
2
+
+ ) : ( +
+
3
+
4
+
+ ); const htmlForTrue = [div(1), div(2)].join(''); @@ -1791,12 +1835,12 @@ describe('Fragment', () => {
  • 0
  • 1
  • - {condition - ? -
  • 2
  • -
  • 3
  • -
    - : null} + {condition ? ( + +
  • 2
  • +
  • 3
  • +
    + ) : null}
  • 4
  • 5
  • @@ -1907,12 +1951,14 @@ describe('Fragment', () => { render() { return (
    - {this.state.error - ?
    Error!
    - :
    -
    1
    - -
    } + {this.state.error ? ( +
    Error!
    + ) : ( +
    +
    1
    + +
    + )}
    ); } @@ -2037,15 +2083,17 @@ describe('Fragment', () => { } render() { - return this.state.active - ? -
    B3
    -
    B4
    -
    - : -
    B1
    -
    B2
    -
    ; + return this.state.active ? ( + +
    B3
    +
    B4
    +
    + ) : ( + +
    B1
    +
    B2
    +
    + ); } } @@ -2203,12 +2251,12 @@ describe('Fragment', () => { } render() { - return this.state.active - ? -
    B1
    -
    B2
    -
    - : null; + return this.state.active ? ( + +
    B1
    +
    B2
    +
    + ) : null; } } @@ -2292,12 +2340,12 @@ describe('Fragment', () => { } render() { - return this.state.active - ? -
    B1
    -
    B2
    -
    - : null; + return this.state.active ? ( + +
    B1
    +
    B2
    +
    + ) : null; } } @@ -2697,24 +2745,26 @@ describe('Fragment', () => { it('should efficiently unmount Fragment children', () => { //
    1 => 1 and Fragment sibling unmounts. Does 1 get correct _nextDom pointer? function App({ condition }) { - return condition - ?
    - -
    1
    -
    2
    -
    - -
    A
    -
    -
    - :
    - -
    1
    -
    - -
    A
    -
    -
    ; + return condition ? ( +
    + +
    1
    +
    2
    +
    + +
    A
    +
    +
    + ) : ( +
    + +
    1
    +
    + +
    A
    +
    +
    + ); } render(, scratch); @@ -2731,29 +2781,31 @@ describe('Fragment', () => { // Fragment wrapping
    2 and
    3 unmounts. Does
    1 get correct // _nextDom pointer to efficiently update DOM? _nextDom should be
    A function App({ condition }) { - return condition - ?
    - -
    1
    - -
    2
    -
    3
    -
    -
    - -
    A
    -
    B
    -
    -
    - :
    - -
    1
    -
    + return condition ? ( +
    + +
    1
    -
    A
    -
    B
    +
    2
    +
    3
    -
    ; + + +
    A
    +
    B
    +
    +
    + ) : ( +
    + +
    1
    +
    + +
    A
    +
    B
    +
    +
    + ); } clearLog(); @@ -2862,30 +2914,32 @@ describe('Fragment', () => { it('should efficiently place new children and unmount nested Fragment children', () => { //
    4 is added and Fragment sibling unmounts. Does
    4 get correct _nextDom pointer? function App({ condition }) { - return condition - ?
    - -
    1
    - -
    2
    -
    3
    -
    -
    - -
    A
    -
    B
    -
    -
    - :
    - -
    1
    -
    4
    -
    + return condition ? ( +
    + +
    1
    -
    A
    -
    B
    +
    2
    +
    3
    -
    ; + + +
    A
    +
    B
    +
    +
    + ) : ( +
    + +
    1
    +
    4
    +
    + +
    A
    +
    B
    +
    +
    + ); } render(, scratch); @@ -2961,29 +3015,31 @@ describe('Fragment', () => { it('should efficiently unmount nested Fragment children when changing node type', () => { //
    1 => 1 and Fragment sibling unmounts. Does 1 get correct _nextDom pointer? function App({ condition }) { - return condition - ?
    - -
    1
    - -
    2
    -
    3
    -
    -
    - -
    A
    -
    B
    -
    -
    - :
    - - 1 - + return condition ? ( +
    + +
    1
    -
    A
    -
    B
    +
    2
    +
    3
    -
    ; + + +
    A
    +
    B
    +
    +
    + ) : ( +
    + + 1 + + +
    A
    +
    B
    +
    +
    + ); } render(, scratch); @@ -3072,22 +3128,24 @@ describe('Fragment', () => { } render() { - return this.state.condition - ? -
    1
    - -
    A
    -
    B
    -
    -
    2
    + return this.state.condition ? ( + +
    1
    + +
    A
    +
    B
    - : - -
    A
    -
    -
    1
    -
    2
    -
    ; +
    2
    +
    + ) : ( + + +
    A
    +
    +
    1
    +
    2
    +
    + ); } } diff --git a/test/browser/keys.test.jsx b/test/browser/keys.test.jsx index 3550352242..443ce99a9e 100644 --- a/test/browser/keys.test.jsx +++ b/test/browser/keys.test.jsx @@ -65,6 +65,7 @@ describe('keys', () => { values.splice(to, 0, value); } + let resetMoveBefore; let resetAppendChild; let resetInsertBefore; let resetRemoveChild; @@ -72,6 +73,7 @@ describe('keys', () => { let resetRemoveText; beforeAll(() => { + resetMoveBefore = logCall(Element.prototype, 'moveBefore'); resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); resetRemoveChild = logCall(Element.prototype, 'removeChild'); @@ -85,6 +87,7 @@ describe('keys', () => { resetRemoveChild(); resetRemove(); resetRemoveText(); + resetMoveBefore(); }); beforeEach(() => { @@ -179,9 +182,11 @@ describe('keys', () => { render({ children, busy }) { return (
    - {children && children.length - ? children - :
    } + {children && children.length ? ( + children + ) : ( +
    + )}
    indicator
    indicator
    @@ -566,19 +571,21 @@ describe('keys', () => { let Stateful2MovedRef; function Foo({ moved }) { - return moved - ?
    -
    1
    - (Stateful1MovedRef = c)} /> -
    2
    - (Stateful2MovedRef = c)} /> -
    - :
    -
    1
    - (Stateful1Ref = c)} /> -
    2
    - (Stateful2Ref = c)} /> -
    ; + return moved ? ( +
    +
    1
    + (Stateful1MovedRef = c)} /> +
    2
    + (Stateful2MovedRef = c)} /> +
    + ) : ( +
    +
    1
    + (Stateful1Ref = c)} /> +
    2
    + (Stateful2Ref = c)} /> +
    + ); } const expectedHtml = div([ @@ -635,31 +642,27 @@ describe('keys', () => { let Stateful2MovedRef; function Foo({ moved }) { - return moved - ?
    -
    1
    - (c ? (Stateful2MovedRef = c) : undefined)} - /> -
    2
    - (c ? (Stateful1MovedRef = c) : undefined)} - /> -
    - :
    -
    1
    - (c ? (Stateful1Ref = c) : undefined)} - /> -
    2
    - (c ? (Stateful2Ref = c) : undefined)} - /> -
    ; + return moved ? ( +
    +
    1
    + (c ? (Stateful2MovedRef = c) : undefined)} + /> +
    2
    + (c ? (Stateful1MovedRef = c) : undefined)} + /> +
    + ) : ( +
    +
    1
    + (c ? (Stateful1Ref = c) : undefined)} /> +
    2
    + (c ? (Stateful2Ref = c) : undefined)} /> +
    + ); } const htmlForFalse = div([ @@ -841,19 +844,21 @@ describe('keys', () => { let Stateful2MovedRef; function Foo({ unkeyed }) { - return unkeyed - ?
    -
    1
    - (Stateful2MovedRef = c)} /> -
    2
    - (Stateful1MovedRef = c)} /> -
    - :
    -
    1
    - (Stateful1Ref = c)} /> -
    2
    - (Stateful2Ref = c)} /> -
    ; + return unkeyed ? ( +
    +
    1
    + (Stateful2MovedRef = c)} /> +
    2
    + (Stateful1MovedRef = c)} /> +
    + ) : ( +
    +
    1
    + (Stateful1Ref = c)} /> +
    2
    + (Stateful2Ref = c)} /> +
    + ); } const expectedHtml = div([ @@ -1007,18 +1012,20 @@ describe('keys', () => { } const App = props => { - return props.y === '2' - ?
    - - - -
    - :
    - {null} - - - -
    ; + return props.y === '2' ? ( +
    + + + +
    + ) : ( +
    + {null} + + + +
    + ); }; render(, scratch); @@ -1040,17 +1047,19 @@ describe('keys', () => { } const App = props => { - return props.y === '1' - ?
    - - - -
    - :
    - - {null} - -
    ; + return props.y === '1' ? ( +
    + + + +
    + ) : ( +
    + + {null} + +
    + ); }; render(, scratch); @@ -1080,18 +1089,20 @@ describe('keys', () => { } const App = props => { - return props.y === '2' - ?
    - - - -
    - :
    - - - {null} - -
    ; + return props.y === '2' ? ( +
    + + + +
    + ) : ( +
    + + + {null} + +
    + ); }; render(, scratch); @@ -1113,18 +1124,20 @@ describe('keys', () => { } const App = props => { - return props.y === '2' - ?
    - - - -
    - :
    - - - - {null} -
    ; + return props.y === '2' ? ( +
    + + + +
    + ) : ( +
    + + + + {null} +
    + ); }; render(, scratch); diff --git a/test/browser/lifecycles/shouldComponentUpdate.test.jsx b/test/browser/lifecycles/shouldComponentUpdate.test.jsx index 3ecd7e8ad3..2d8618b277 100644 --- a/test/browser/lifecycles/shouldComponentUpdate.test.jsx +++ b/test/browser/lifecycles/shouldComponentUpdate.test.jsx @@ -15,10 +15,13 @@ describe('Lifecycle methods', () => { // expect(getLog()).to.deep.equal(expectedOperations, message); // } let resetInsertBefore; + let resetMoveBefore; let resetRemoveChild; let resetRemove; beforeAll(() => { + // @ts-expect-error + resetMoveBefore = logCall(Element.prototype, 'moveBefore'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); resetRemoveChild = logCall(Element.prototype, 'appendChild'); resetRemove = logCall(Element.prototype, 'removeChild'); @@ -28,6 +31,7 @@ describe('Lifecycle methods', () => { resetInsertBefore(); resetRemoveChild(); resetRemove(); + resetMoveBefore(); }); beforeEach(() => { diff --git a/test/browser/render.test.jsx b/test/browser/render.test.jsx index 0139661726..c40c8fe0aa 100644 --- a/test/browser/render.test.jsx +++ b/test/browser/render.test.jsx @@ -30,6 +30,7 @@ describe('render()', () => { let resetInsertBefore; let resetRemoveText; let resetRemove; + let resetMoveBefore; beforeEach(() => { scratch = setupScratch(); @@ -43,12 +44,15 @@ describe('render()', () => { beforeAll(() => { resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); + // @ts-expect-error + resetMoveBefore = logCall(Element.prototype, 'moveBefore'); resetRemoveText = logCall(Text.prototype, 'remove'); resetRemove = logCall(Element.prototype, 'remove'); }); afterAll(() => { resetAppendChild(); + resetMoveBefore(); resetInsertBefore(); resetRemoveText(); resetRemove(); @@ -435,19 +439,21 @@ describe('render()', () => { render() { return (
    - {this.state.active - ? - - - -
    - Foo -
    - : - - - -
    Foo
    } + {this.state.active ? ( + + + + +
    + Foo +
    + ) : ( + + + + +
    Foo
    + )}
    ); } @@ -664,9 +670,11 @@ describe('render()', () => { } render(props, { html }) { // eslint-disable-next-line react/no-danger - return html - ?
    - :
    ; + return html ? ( +
    + ) : ( +
    + ); } } @@ -1879,14 +1887,16 @@ describe('render()', () => { // // We insert which should amount to a skew of -1 which should // make us correctly match the X component. - return condition - ?
    - - -
    - :
    - -
    ; + return condition ? ( +
    + + +
    + ) : ( +
    + +
    + ); } render(, scratch); From 3b083af0a33cc3041e97e3b53262d2611d67d8d2 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Fri, 24 Oct 2025 20:08:14 +0200 Subject: [PATCH 2/2] Add try block --- src/diff/children.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index b169f54ef6..0c70e77558 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -366,11 +366,19 @@ function insert(parentVNode, oldDom, parentDom, shouldPlace, isMounting) { oldDom = getDomSibling(parentVNode); } + const target = oldDom || NULL; if (HAS_MOVE_BEFORE_SUPPORT && !isMounting) { - // @ts-expect-error This isn't added to TypeScript lib.d.ts yet - parentDom.moveBefore(parentVNode._dom, oldDom); + try { + // Technically this can throw in multiple scenario's one of which + // is a component that is mounting - we could consider removing + // the condition because we have a catch in place. + // @ts-expect-error This isn't added to TypeScript lib.d.ts yet + parentDom.moveBefore(parentVNode._dom, target); + } catch (error) { + parentDom.insertBefore(parentVNode._dom, target); + } } else { - parentDom.insertBefore(parentVNode._dom, oldDom || NULL); + parentDom.insertBefore(parentVNode._dom, target); } } oldDom = parentVNode._dom;