Skip to content

Commit

Permalink
fix: address scenario where we would crash when replacing a matched v…
Browse files Browse the repository at this point in the history
…node with null (#4281)

* add test for the issue, TODO convert to class components later

* wip

* correct assertion

* avoid crasshing when replacing existing node

* move test to core
  • Loading branch information
JoviDeCroock committed Feb 16, 2024
1 parent f808dcb commit 2f44635
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 4 deletions.
10 changes: 6 additions & 4 deletions src/diff/children.js
Expand Up @@ -62,7 +62,6 @@ export function diffChildren(

for (i = 0; i < newChildrenLength; i++) {
childVNode = newParentVNode._children[i];

if (
childVNode == null ||
typeof childVNode == 'boolean' ||
Expand Down Expand Up @@ -231,11 +230,15 @@ function constructNewChildrenArray(newParentVNode, renderResult, oldChildren) {
// Handle unmounting null placeholders, i.e. VNode => null in unkeyed children
if (childVNode == null) {
oldVNode = oldChildren[i];
if (oldVNode && oldVNode.key == null && oldVNode._dom) {
if (
oldVNode &&
oldVNode.key == null &&
oldVNode._dom &&
(oldVNode._flags & MATCHED) === 0
) {
if (oldVNode._dom == newParentVNode._nextDom) {
newParentVNode._nextDom = getDomSibling(oldVNode);
}

unmount(oldVNode, oldVNode, false);

// Explicitly nullify this position in oldChildren instead of just
Expand All @@ -250,7 +253,6 @@ function constructNewChildrenArray(newParentVNode, renderResult, oldChildren) {
oldChildren[i] = null;
remainingOldChildren--;
}

continue;
}

Expand Down
48 changes: 48 additions & 0 deletions test/browser/render.test.js
Expand Up @@ -1308,4 +1308,52 @@ describe('render()', () => {
expect(divs[1].hasAttribute('role')).to.equal(false);
expect(divs[2].hasAttribute('role')).to.equal(false);
});

it('should not crash or repeatedly add the same child when replacing a matched vnode with null', () => {
const B = () => <div>B</div>;

let update;
class App extends Component {
constructor(props) {
super(props);
this.state = { show: true };
update = () => {
this.setState(state => ({ show: !state.show }));
};
}

render() {
if (this.state.show) {
return (
<div>
<B />
<div />
</div>
);
}
return (
<div>
<div />
{null}
<B />
</div>
);
}
}

render(<App />, scratch);
expect(scratch.innerHTML).to.equal('<div><div>B</div><div></div></div>');

update();
rerender();
expect(scratch.innerHTML).to.equal('<div><div></div><div>B</div></div>');

update();
rerender();
expect(scratch.innerHTML).to.equal('<div><div>B</div><div></div></div>');

update();
rerender();
expect(scratch.innerHTML).to.equal('<div><div></div><div>B</div></div>');
});
});

0 comments on commit 2f44635

Please sign in to comment.