From db4da7a844d21fff495a58fbc9cfb2247042240a Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 31 Dec 2020 13:13:53 -0800 Subject: [PATCH] Fix child reordering with memo --- compat/test/browser/memo.test.js | 65 ++++++++++++++++++++++++++++++-- src/diff/children.js | 6 ++- test/browser/fragments.test.js | 2 +- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/compat/test/browser/memo.test.js b/compat/test/browser/memo.test.js index 7451b4f39fd..c1e155c429c 100644 --- a/compat/test/browser/memo.test.js +++ b/compat/test/browser/memo.test.js @@ -1,11 +1,26 @@ import { setupRerender } from 'preact/test-utils'; -import { setupScratch, teardown } from '../../../test/_util/helpers'; -import React, { createElement, Component, render, memo } from 'preact/compat'; +import { + createEvent, + setupScratch, + teardown +} from '../../../test/_util/helpers'; +import React, { + createElement, + Component, + render, + memo, + useState +} from 'preact/compat'; +import { li, ol } from '../../../test/_util/dom'; const h = React.createElement; describe('memo()', () => { - let scratch, rerender; + /** @type {HTMLDivElement} */ + let scratch; + + /** @type {() => void} */ + let rerender; beforeEach(() => { scratch = setupScratch(); @@ -172,4 +187,48 @@ describe('memo()', () => { expect(ref.current).not.to.be.undefined; expect(ref.current).to.be.instanceOf(Foo); }); + + it('should not unnecessarily reorder children #2895', () => { + const array = [{ name: 'A' }, { name: 'B' }, { name: 'C' }, { name: 'D' }]; + + const List = () => { + const [selected, setSelected] = useState(''); + return ( +
    + {array.map(item => ( + + ))} +
+ ); + }; + + const ListItem = memo(({ name, isSelected, setSelected }) => { + const handleClick = () => setSelected(name); + return ( +
  • + {name} +
  • + ); + }); + + render(, scratch); + expect(scratch.innerHTML).to.equal( + `
    1. A
    2. B
    3. C
    4. D
    ` + ); + + let listItem = scratch.querySelector('li:nth-child(3)'); + listItem.dispatchEvent(createEvent('click')); + rerender(); + + expect(scratch.innerHTML).to.equal( + `
    1. A
    2. B
    3. C
    4. D
    ` + ); + }); }); diff --git a/src/diff/children.js b/src/diff/children.js index e9425607fa1..3d6516c1d7b 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -264,10 +264,14 @@ function reorderChildren(childVNode, oldDom, parentDom) { for (let tmp = 0; tmp < childVNode._children.length; tmp++) { let vnode = childVNode._children[tmp]; if (vnode) { + // We typically enter this code path on sCU bailout, where we copy + // oldVNode._children to newVNode._children. If that is the case, we need + // to update the old children's _parent pointer to point to the newVNode + // (childVNode here). vnode._parent = childVNode; if (typeof vnode.type == 'function') { - reorderChildren(vnode, oldDom, parentDom); + oldDom = reorderChildren(vnode, oldDom, parentDom); } else { oldDom = placeChild( parentDom, diff --git a/test/browser/fragments.test.js b/test/browser/fragments.test.js index a0be8a20057..882aeed9550 100644 --- a/test/browser/fragments.test.js +++ b/test/browser/fragments.test.js @@ -568,7 +568,7 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal('
    Hello
    '); }); - it.skip('should preserve state between double nested fragment and double nested array', () => { + it('should preserve state between double nested fragment and double nested array', () => { function Foo({ condition }) { return condition ? (