Skip to content

Commit c6a669a

Browse files
committed
Fix suspense crash
1 parent 6670a4a commit c6a669a

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

compat/src/suspense.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ Suspense.prototype.render = function (props, state) {
236236
* @returns {((unsuspend: () => void) => void)?}
237237
*/
238238
export function suspended(vnode) {
239+
if (!vnode._parent) return null;
239240
/** @type {import('./internal').Component} */
240241
let component = vnode._parent._component;
241242
return component && component._suspended && component._suspended(vnode);

compat/test/browser/suspense.test.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,6 +2188,129 @@ describe('suspense', () => {
21882188
});
21892189
});
21902190

2191+
it('should not crash when suspended child updates after unmount', () => {
2192+
let childInstance = null;
2193+
const neverResolvingPromise = new Promise(() => {});
2194+
2195+
class ThrowingChild extends Component {
2196+
constructor(props) {
2197+
super(props);
2198+
this.state = { suspend: false, value: 0 };
2199+
childInstance = this;
2200+
}
2201+
2202+
render(props, state) {
2203+
if (state.suspend) {
2204+
throw neverResolvingPromise;
2205+
}
2206+
return <div>value:{state.value}</div>;
2207+
}
2208+
}
2209+
2210+
render(
2211+
<Suspense fallback={<div>Suspended...</div>}>
2212+
<ThrowingChild />
2213+
</Suspense>,
2214+
scratch
2215+
);
2216+
2217+
expect(scratch.innerHTML).to.equal('<div>value:0</div>');
2218+
2219+
childInstance.setState({ suspend: true });
2220+
rerender();
2221+
expect(scratch.innerHTML).to.equal('<div>Suspended...</div>');
2222+
2223+
render(null, scratch);
2224+
expect(scratch.innerHTML).to.equal('');
2225+
2226+
childInstance.setState({ value: 1 });
2227+
rerender();
2228+
2229+
expect(scratch.innerHTML).to.equal('');
2230+
});
2231+
2232+
it('should not crash when suspense promise resolves after unmount', () => {
2233+
let resolve;
2234+
const promise = new Promise(r => {
2235+
resolve = r;
2236+
});
2237+
2238+
class ThrowingChild extends Component {
2239+
render() {
2240+
throw promise;
2241+
}
2242+
}
2243+
2244+
render(
2245+
<Suspense fallback={<div>Suspended...</div>}>
2246+
<ThrowingChild />
2247+
</Suspense>,
2248+
scratch
2249+
);
2250+
rerender();
2251+
2252+
expect(scratch.innerHTML).to.equal('<div>Suspended...</div>');
2253+
2254+
render(null, scratch);
2255+
expect(scratch.innerHTML).to.equal('');
2256+
2257+
resolve();
2258+
2259+
return promise.then(() => {
2260+
rerender();
2261+
expect(scratch.innerHTML).to.equal('');
2262+
});
2263+
});
2264+
2265+
it('should not crash when useContext is used in a suspending component', () => {
2266+
const TestContext = createContext('default');
2267+
let resolve;
2268+
let shouldSuspend = false;
2269+
const promise = new Promise(r => {
2270+
resolve = r;
2271+
});
2272+
2273+
function ContextUser() {
2274+
const value = React.useContext(TestContext);
2275+
if (shouldSuspend) {
2276+
throw promise;
2277+
}
2278+
return <div>Context: {value}</div>;
2279+
}
2280+
2281+
render(
2282+
<TestContext.Provider value="test-value">
2283+
<Suspense fallback={<div>Suspended...</div>}>
2284+
<ContextUser />
2285+
</Suspense>
2286+
</TestContext.Provider>,
2287+
scratch
2288+
);
2289+
2290+
expect(scratch.innerHTML).to.equal('<div>Context: test-value</div>');
2291+
2292+
shouldSuspend = true;
2293+
render(
2294+
<TestContext.Provider value="test-value">
2295+
<Suspense fallback={<div>Suspended...</div>}>
2296+
<ContextUser />
2297+
</Suspense>
2298+
</TestContext.Provider>,
2299+
scratch
2300+
);
2301+
rerender();
2302+
2303+
expect(scratch.innerHTML).to.equal('<div>Suspended...</div>');
2304+
2305+
shouldSuspend = false;
2306+
resolve();
2307+
2308+
return promise.then(() => {
2309+
rerender();
2310+
expect(scratch.innerHTML).to.equal('<div>Context: test-value</div>');
2311+
});
2312+
});
2313+
21912314
it('should not crash if fallback has same DOM as suspended nodes', () => {
21922315
const [Lazy, resolveLazy] = createLazy();
21932316

0 commit comments

Comments
 (0)