Skip to content

Commit

Permalink
Merge 000933a into e206b07
Browse files Browse the repository at this point in the history
  • Loading branch information
sventschui committed May 4, 2019
2 parents e206b07 + 000933a commit bd55e04
Show file tree
Hide file tree
Showing 6 changed files with 440 additions and 4 deletions.
6 changes: 4 additions & 2 deletions compat/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render as preactRender, cloneElement as preactCloneElement, createRef, h, Component, options, toChildArray, createContext, Fragment } from 'preact';
import { render as preactRender, cloneElement as preactCloneElement, createRef, h, Component, options, toChildArray, createContext, Fragment, Suspense, lazy } from 'preact';
import * as hooks from 'preact/hooks';
export * from 'preact/hooks';
import { assign } from '../../src/util';
Expand Down Expand Up @@ -401,5 +401,7 @@ export default assign({
PureComponent,
memo,
forwardRef,
unstable_batchedUpdates
unstable_batchedUpdates,
Suspense,
lazy
}, hooks);
30 changes: 29 additions & 1 deletion src/diff/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi

try {
outer: if (oldVNode.type===Fragment || newType===Fragment) {
// Passing the ancestorComponent here is needed for catchErrorInComponent to properly traverse upwards through fragments
// to find a parent Suspense
c = { _ancestorComponent: ancestorComponent };
diffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, c, oldDom);

// Mark dom as empty in case `_children` is any empty array. If it isn't
Expand Down Expand Up @@ -362,10 +365,22 @@ function doRender(props, state, context) {
* component check for error boundary behaviors
*/
function catchErrorInComponent(error, component) {
// thrown Promises are meant to suspend...
let isSuspend = typeof error.then === 'function';
let suspendingComponent = component;

for (; component; component = component._ancestorComponent) {
if (!component._processingException) {
try {
if (component.constructor.getDerivedStateFromError!=null) {
if (isSuspend) {
if (component.__s) {
component.__s(error);
}
else {
continue;
}
}
else if (component.constructor.getDerivedStateFromError!=null) {
component.setState(component.constructor.getDerivedStateFromError(error));
}
else if (component.componentDidCatch!=null) {
Expand All @@ -378,8 +393,21 @@ function catchErrorInComponent(error, component) {
}
catch (e) {
error = e;
isSuspend = typeof error.then === 'function';
suspendingComponent = component;
}
}
}

// TODO: Add a react-like error message to preact/debug
/*
[componentName] suspended while rendering, but no fallback UI was specified.
Add a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.
*/
if (isSuspend) {
return catchErrorInComponent(new Error('Missing Suspense'), suspendingComponent);
}

throw error;
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export { Component } from './component';
export { cloneElement } from './clone-element';
export { createContext } from './create-context';
export { toChildArray } from './diff/children';
export { Suspense, lazy } from './suspense';
export { default as options } from './options';
53 changes: 53 additions & 0 deletions src/suspense.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Component } from './component';
import { createElement } from './create-element';

export class Suspense extends Component {
// TODO: can we drop this constructor / state init?
constructor(props) {
super(props);

this.state = {};
}

__s(e) {
if (e.then == 'function') {
this.setState({ l: 1 });
const cb = () => { this.setState({ l: 0 }); };

// Suspense ignores errors thrown in Promises as this should be handled by user land code
e.then(cb, cb);
}
else {
throw e;
}
}

render(props, state) {
return state.l ? props.fallback : props.children;
}
}

export function lazy(loader) {
let prom;
let component;
let error;
return function L(props) {
if (!prom) {
prom = loader();
prom.then(
({ default: c }) => { component = c; },
e => error = e,
);
}

if (error) {
throw error;
}

if (!component) {
throw prom;
}

return createElement(component, props);
};
}
11 changes: 10 additions & 1 deletion test/browser/lifecycle.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { setupRerender } from 'preact/test-utils';
import { createElement as h, render, Component } from '../../src/index';
import { createElement as h, render, Component, Fragment } from '../../src/index';
import { setupScratch, teardown } from '../_util/helpers';

/** @jsx h */
Expand Down Expand Up @@ -2245,6 +2245,15 @@ describe('Lifecycle methods', () => {
expect(Receiver.prototype.componentDidCatch).to.have.been.called;
});

it('should be called when child inside a Fragment fails', () => {
function ThrowErr() {
throw new Error('Error!');
}

render(<Receiver><Fragment><ThrowErr /></Fragment></Receiver>, scratch);
expect(Receiver.prototype.componentDidCatch).to.have.been.called;
});

it('should re-render with new content', () => {
class ThrowErr extends Component {
componentWillMount() {
Expand Down
Loading

0 comments on commit bd55e04

Please sign in to comment.