Skip to content

Commit dec937f

Browse files
committed
Ensure we rerender after a suspensefully hydrating boundary throws an error
1 parent a9b12d9 commit dec937f

File tree

2 files changed

+65
-0
lines changed

2 files changed

+65
-0
lines changed

compat/test/browser/suspense-hydration.test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,64 @@ describe('suspense hydration', () => {
9797
});
9898
});
9999

100+
it('Should not crash when oldVNode._children is null during shouldComponentUpdate optimization', () => {
101+
const originalHtml = '<div>Hello</div>';
102+
scratch.innerHTML = originalHtml;
103+
clearLog();
104+
105+
class ErrorBoundary extends React.Component {
106+
constructor(props) {
107+
super(props);
108+
this.state = { hasError: false };
109+
}
110+
111+
static getDerivedStateFromError() {
112+
return { hasError: true };
113+
}
114+
115+
render() {
116+
return this.props.children;
117+
}
118+
}
119+
120+
const [Lazy, resolve] = createLazy();
121+
function App() {
122+
return (
123+
<Suspense>
124+
<ErrorBoundary>
125+
<Lazy />
126+
</ErrorBoundary>
127+
</Suspense>
128+
);
129+
}
130+
131+
hydrate(<App />, scratch);
132+
rerender(); // Flush rerender queue to mimic what preact will really do
133+
expect(scratch.innerHTML).to.equal(originalHtml);
134+
expect(getLog()).to.deep.equal([]);
135+
clearLog();
136+
137+
let i = 0;
138+
class ThrowOrRender extends React.Component {
139+
shouldComponentUpdate() {
140+
return i === 0;
141+
}
142+
render() {
143+
if (i === 0) {
144+
i++;
145+
throw new Error('Test error');
146+
}
147+
return <div>Hello</div>;
148+
}
149+
}
150+
151+
return resolve(ThrowOrRender).then(() => {
152+
rerender();
153+
expect(scratch.innerHTML).to.equal(originalHtml);
154+
clearLog();
155+
});
156+
});
157+
100158
it('should leave DOM untouched when suspending while hydrating', () => {
101159
scratch.innerHTML = '<!-- test --><div>Hello</div>';
102160
clearLog();

src/diff/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,12 @@ export function diff(
309309
for (let i = excessDomChildren.length; i--; ) {
310310
removeNode(excessDomChildren[i]);
311311
}
312+
markAsForce(newVNode);
312313
}
313314
} else {
314315
newVNode._dom = oldVNode._dom;
315316
newVNode._children = oldVNode._children;
317+
markAsForce(newVNode);
316318
}
317319
options._catchError(e, newVNode, oldVNode);
318320
}
@@ -341,6 +343,11 @@ export function diff(
341343
return newVNode._flags & MODE_SUSPENDED ? undefined : oldDom;
342344
}
343345

346+
function markAsForce(vnode) {
347+
if (vnode && vnode._component) vnode._component._force = true;
348+
if (vnode && vnode._children) vnode._children.forEach(markAsForce);
349+
}
350+
344351
/**
345352
* @param {Array<Component>} commitQueue List of components
346353
* which have callbacks to invoke in commitRoot

0 commit comments

Comments
 (0)