Skip to content

Commit

Permalink
Merge b8bb86e into e206b07
Browse files Browse the repository at this point in the history
  • Loading branch information
sventschui committed May 3, 2019
2 parents e206b07 + b8bb86e commit 76f96dd
Show file tree
Hide file tree
Showing 5 changed files with 364 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);
50 changes: 48 additions & 2 deletions src/diff/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { diffChildren } from './children';
import { diffProps } from './props';
import { assign, removeNode } from '../util';
import options from '../options';
import { sym as suspenseSymbol } from '../suspense';

/**
* Diff two virtual nodes and apply proper changes to the DOM
Expand All @@ -26,7 +27,19 @@ import options from '../options';
export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, force, oldDom) {
// If the previous type doesn't match the new type we drop the whole subtree
if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type || oldVNode.key!==newVNode.key) {
if (oldVNode!=null) unmount(oldVNode, ancestorComponent);
if (oldVNode!=null) {
const ancestor = oldVNode._component && oldVNode._component._ancestorComponent;
if (ancestor && ancestor[suspenseSymbol]) {
ancestor._vnode.suspendedContent = oldVNode;
// TODO: check whether parent was already removed from DOM...
if (oldVNode._dom) {
removeNode(oldVNode._dom);
}
}
else {
unmount(oldVNode, ancestorComponent);
}
}
if (newVNode==null) return null;
oldVNode = EMPTY_OBJ;
}
Expand Down Expand Up @@ -140,6 +153,11 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi

let prev = c._prevVNode || null;
c._dirty = false;

if (c[suspenseSymbol] === suspenseSymbol && c._vnode.suspendedContent) {
if (prev) { unmount(prev); }
prev = c._vnode.suspendedContent;
}
let vnode = c._prevVNode = coerceToVNode(c.render(c.props, c.state, c.context));

if (c.getChildContext!=null) {
Expand All @@ -151,6 +169,7 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi
}

c._depth = ancestorComponent ? (ancestorComponent._depth || 0) + 1 : 0;
// this creates a new WrapperOne / new CustomSuspense
c.base = newVNode._dom = diff(parentDom, vnode, prev, context, isSvg, excessDomChildren, mounts, c, null, oldDom);

if (vnode!=null) {
Expand Down Expand Up @@ -362,10 +381,24 @@ 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) {
// console.log('catchErrorInComponent component[suspenseSymbol]', component[suspenseSymbol]);
if (component[suspenseSymbol] === suspenseSymbol && component.componentDidCatch!=null) {
// console.log('hitting suspense...');
component.componentDidCatch(error);
}
else {
continue;
}
}
else if (component.constructor.getDerivedStateFromError!=null) {
component.setState(component.constructor.getDerivedStateFromError(error));
}
else if (component.componentDidCatch!=null) {
Expand All @@ -378,8 +411,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';
65 changes: 65 additions & 0 deletions src/suspense.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Component } from './component';
import { createElement } from './create-element';

// TODO: react warns in dev mode about defaultProps and propTypes not being supported on lazy
// loaded components

export const sym = '_s';

export const Suspense2 = 'suspense';

export class Suspense extends Component {
constructor(props) {
// TODO: should we add propTypes in DEV mode?
super(props);

// mark this component as a handler of suspension (thrown Promises)
this[sym] = sym;

this.state = {
l: false
};
}

componentDidCatch(e) {
if (e && typeof e.then === 'function') {
this.setState({ l: true });
const cb = () => { this.setState({ l: false }); };

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

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

export function lazy(loader) {
let prom;
let component;
let error;
return function Lazy(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);
};
}
Loading

0 comments on commit 76f96dd

Please sign in to comment.