Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion compat/src/suspense.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const oldUnmount = options.unmount;
options.unmount = function (vnode) {
/** @type {import('./internal').Component} */
const component = vnode._component;
if (component) component._unmounted = true;
if (component && component._onResolve) {
component._onResolve();
}
Expand Down Expand Up @@ -129,7 +130,7 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) {

let resolved = false;
const onResolved = () => {
if (resolved) return;
if (resolved || c._unmounted) return;

resolved = true;
suspendingComponent._onResolve = null;
Expand Down Expand Up @@ -236,6 +237,7 @@ Suspense.prototype.render = function (props, state) {
* @returns {((unsuspend: () => void) => void)?}
*/
export function suspended(vnode) {
if (!vnode._parent) return null;
/** @type {import('./internal').Component} */
let component = vnode._parent._component;
return component && component._suspended && component._suspended(vnode);
Expand Down
277 changes: 277 additions & 0 deletions compat/test/browser/suspense.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, {
} from 'preact/compat';
import { setupScratch, teardown } from '../../../test/_util/helpers';
import { createLazy, createSuspender } from './suspense-utils';
import { expect } from 'chai';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe nit: is this needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whups, darn vscode auto-import


const h = React.createElement;
/* eslint-env browser, mocha */
Expand Down Expand Up @@ -2188,6 +2189,282 @@ describe('suspense', () => {
});
});

it('should not crash when suspended child updates after unmount', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uncommenting line 240 will surface the crash of this

let childInstance = null;
const neverResolvingPromise = new Promise(() => {});

class ThrowingChild extends Component {
constructor(props) {
super(props);
this.state = { suspend: false, value: 0 };
childInstance = this;
}

render(props, state) {
if (state.suspend) {
throw neverResolvingPromise;
}
return <div>value:{state.value}</div>;
}
}

render(
<Suspense fallback={<div>Suspended...</div>}>
<ThrowingChild />
</Suspense>,
scratch
);

expect(scratch.innerHTML).to.equal('<div>value:0</div>');

childInstance.setState({ suspend: true });
rerender();
expect(scratch.innerHTML).to.equal('<div>Suspended...</div>');

render(null, scratch);
expect(scratch.innerHTML).to.equal('');

childInstance.setState({ value: 1 });
rerender();

expect(scratch.innerHTML).to.equal('');
});

it('should not crash when suspended child updates after diffed unmount', () => {
let childInstance = null;
const neverResolvingPromise = new Promise(() => {});

class ThrowingChild extends Component {
constructor(props) {
super(props);
this.state = { suspend: false, value: 0 };
childInstance = this;
}

render(props, state) {
if (state.suspend) {
throw neverResolvingPromise;
}
return <div>value:{state.value}</div>;
}
}

const HelloWorld = () => <p>Hello world</p>;

let set;
const App = () => {
const [show, setShow] = useState(true);
set = setShow;
return show ? (
<Suspense fallback={<div>Suspended...</div>}>
<ThrowingChild />
</Suspense>
) : (
<HelloWorld />
);
};

render(<App />, scratch);

expect(scratch.innerHTML).to.equal('<div>value:0</div>');

childInstance.setState({ suspend: true });
rerender();
expect(scratch.innerHTML).to.equal('<div>Suspended...</div>');

set(false);
rerender();
expect(scratch.innerHTML).to.equal('<p>Hello world</p>');

childInstance.setState({ value: 1 });
rerender();

expect(scratch.innerHTML).to.equal('<p>Hello world</p>');
});

it('should not crash when suspended child resolves after unmount', async () => {
let childInstance = null,
res;
const neverResolvingPromise = new Promise(r => {
res = r;
});

class ThrowingChild extends Component {
constructor(props) {
super(props);
this.state = { suspend: false, value: 0 };
childInstance = this;
}

render(props, state) {
if (state.suspend) {
throw neverResolvingPromise;
}
return <div>value:{state.value}</div>;
}
}

render(
<Suspense fallback={<div>Suspended...</div>}>
<ThrowingChild />
</Suspense>,
scratch
);

expect(scratch.innerHTML).to.equal('<div>value:0</div>');

childInstance.setState({ suspend: true });
rerender();
expect(scratch.innerHTML).to.equal('<div>Suspended...</div>');

render(null, scratch);
expect(scratch.innerHTML).to.equal('');

res();
return neverResolvingPromise.then(() => {
rerender();
expect(scratch.innerHTML).to.equal('');
});
});

it('should not crash when suspended child resolves after diffed unmount', async () => {
let childInstance = null,
res;
const neverResolvingPromise = new Promise(r => {
res = r;
});

class ThrowingChild extends Component {
constructor(props) {
super(props);
this.state = { suspend: false, value: 0 };
childInstance = this;
}

render(props, state) {
if (state.suspend) {
throw neverResolvingPromise;
}
return <div>value:{state.value}</div>;
}
}

const HelloWorld = () => <p>Hello world</p>;

let set;
const App = () => {
const [show, setShow] = useState(true);
set = setShow;
return show ? (
<Suspense fallback={<div>Suspended...</div>}>
<ThrowingChild />
</Suspense>
) : (
<HelloWorld />
);
};

render(<App />, scratch);

expect(scratch.innerHTML).to.equal('<div>value:0</div>');

childInstance.setState({ suspend: true });
rerender();
expect(scratch.innerHTML).to.equal('<div>Suspended...</div>');

set(false);
rerender();
expect(scratch.innerHTML).to.equal('<p>Hello world</p>');

res();
return neverResolvingPromise.then(() => {
rerender();
expect(scratch.innerHTML).to.equal('<p>Hello world</p>');
});
});

it('should not crash when suspense promise resolves after unmount', () => {
let resolve;
const promise = new Promise(r => {
resolve = r;
});

class ThrowingChild extends Component {
render() {
throw promise;
}
}

render(
<Suspense fallback={<div>Suspended...</div>}>
<ThrowingChild />
</Suspense>,
scratch
);
rerender();

expect(scratch.innerHTML).to.equal('<div>Suspended...</div>');

render(null, scratch);
expect(scratch.innerHTML).to.equal('');

resolve();

return promise.then(() => {
rerender();
expect(scratch.innerHTML).to.equal('');
});
});

it('should not crash when useContext is used in a suspending component', () => {
const TestContext = createContext('default');
let resolve;
let shouldSuspend = false;
const promise = new Promise(r => {
resolve = r;
});

function ContextUser() {
const value = React.useContext(TestContext);
if (shouldSuspend) {
throw promise;
}
return <div>Context: {value}</div>;
}

render(
<TestContext.Provider value="test-value">
<Suspense fallback={<div>Suspended...</div>}>
<ContextUser />
</Suspense>
</TestContext.Provider>,
scratch
);

expect(scratch.innerHTML).to.equal('<div>Context: test-value</div>');

shouldSuspend = true;
render(
<TestContext.Provider value="test-value">
<Suspense fallback={<div>Suspended...</div>}>
<ContextUser />
</Suspense>
</TestContext.Provider>,
scratch
);
rerender();

expect(scratch.innerHTML).to.equal('<div>Suspended...</div>');

shouldSuspend = false;
resolve();

return promise.then(() => {
rerender();
expect(scratch.innerHTML).to.equal('<div>Context: test-value</div>');
});
});

it('should not crash if fallback has same DOM as suspended nodes', () => {
const [Lazy, resolveLazy] = createLazy();

Expand Down
1 change: 1 addition & 0 deletions mangle.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"$_children": "__k",
"$_pendingSuspensionCount": "__u",
"$_childDidSuspend": "__c",
"$_unmounted": "__z",
"$_onResolve": "__R",
"$_suspended": "__a",
"$_dom": "__e",
Expand Down
Loading