Skip to content

Commit 82ed24e

Browse files
authored
Fix hydrate error recovery with null excess DOM children (#5112)
1 parent 4299769 commit 82ed24e

2 files changed

Lines changed: 77 additions & 3 deletions

File tree

src/diff/index.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,11 +294,15 @@ export function diff(
294294
oldDom = oldDom.nextSibling;
295295
}
296296

297-
excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL;
297+
if (excessDomChildren != NULL) {
298+
excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL;
299+
}
298300
newVNode._dom = oldDom;
299301
} else {
300-
for (let i = excessDomChildren.length; i--; ) {
301-
removeNode(excessDomChildren[i]);
302+
if (excessDomChildren != NULL) {
303+
for (let i = excessDomChildren.length; i--; ) {
304+
removeNode(excessDomChildren[i]);
305+
}
302306
}
303307
markAsForce(newVNode);
304308
}

test/browser/hydrate.test.jsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,4 +529,74 @@ describe('hydrate()', () => {
529529
rerender();
530530
expect(scratch.innerHTML).to.equal('<div>Error!</div>');
531531
});
532+
533+
it('should not crash when hydrating a suspending component without excess DOM children', () => {
534+
const promise = Promise.resolve();
535+
let caught;
536+
537+
class Boundary extends Component {
538+
componentDidCatch(error) {
539+
caught = error;
540+
this.setState({ error });
541+
}
542+
543+
render() {
544+
return this.state && this.state.error ? (
545+
<div>Loading</div>
546+
) : (
547+
<textarea defaultValue="">
548+
<Suspender />
549+
</textarea>
550+
);
551+
}
552+
}
553+
554+
function Suspender() {
555+
throw promise;
556+
}
557+
558+
scratch.innerHTML = '<textarea></textarea>';
559+
560+
expect(() => {
561+
hydrate(<Boundary />, scratch);
562+
}).to.not.throw();
563+
564+
expect(caught).to.equal(promise);
565+
});
566+
567+
it('should pass the original error to boundaries when hydrating without excess DOM children', () => {
568+
let caught;
569+
const error = new Error('real error');
570+
571+
class Boundary extends Component {
572+
componentDidCatch(error) {
573+
caught = error;
574+
this.setState({ error });
575+
}
576+
577+
render() {
578+
return this.state && this.state.error ? <div>Error!</div> : <Wrapper />;
579+
}
580+
}
581+
582+
function Wrapper() {
583+
return (
584+
<textarea defaultValue="">
585+
<Boom />
586+
</textarea>
587+
);
588+
}
589+
590+
function Boom() {
591+
throw error;
592+
}
593+
594+
scratch.innerHTML = '<textarea></textarea>';
595+
596+
hydrate(<Boundary />, scratch);
597+
rerender();
598+
599+
expect(caught).to.equal(error);
600+
expect(caught.message).to.equal('real error');
601+
});
532602
});

0 commit comments

Comments
 (0)