Skip to content

Commit

Permalink
Merge branch 'master' into feat/putt-putt-styles
Browse files Browse the repository at this point in the history
  • Loading branch information
cristianbote committed May 9, 2019
2 parents 8fc6122 + a23b921 commit a9b8948
Show file tree
Hide file tree
Showing 11 changed files with 763 additions and 9 deletions.
10 changes: 7 additions & 3 deletions compat/src/index.js
@@ -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 @@ -376,7 +376,9 @@ export {
memo,
forwardRef,
// eslint-disable-next-line camelcase
unstable_batchedUpdates
unstable_batchedUpdates,
Suspense,
lazy
};

// React copies the named exports to the default one.
Expand All @@ -399,5 +401,7 @@ export default assign({
PureComponent,
memo,
forwardRef,
unstable_batchedUpdates
unstable_batchedUpdates,
Suspense,
lazy
}, hooks);
16 changes: 16 additions & 0 deletions debug/src/debug.js
Expand Up @@ -74,6 +74,22 @@ export function initDebug() {

// Check prop-types if available
if (typeof vnode.type==='function' && vnode.type.propTypes) {
if (vnode.type.displayName === 'Lazy') {
const m = 'PropTypes are not supported on lazy(). Use propTypes on the wrapped component itself. ';
try {
const lazyVNode = vnode.type();
console.warn(m + 'Component wrapped in lazy() is ' + (lazyVNode.type.displayName || lazyVNode.type.name));
}
catch (promise) {
console.warn(m + 'We will log the wrapped component\'s name once it is loaded.');
if (promise.then) {
promise.then((exports) => {
console.warn('Component wrapped in lazy() is ' + (exports.default.displayName || exports.default.name));
});
}

}
}
checkPropTypes(vnode.type.propTypes, vnode.props, getDisplayName(vnode), serializeVNode(vnode));
}

Expand Down
121 changes: 119 additions & 2 deletions debug/test/browser/debug.test.js
@@ -1,6 +1,6 @@
import { createElement as h, options, render, createRef, Component, Fragment } from 'preact';
import { createElement as h, options, render, createRef, Component, Fragment, lazy, Suspense } from 'preact';
import { useState, useEffect, useLayoutEffect, useMemo, useCallback } from 'preact/hooks';
import { act } from 'preact/test-utils';
import { act, setupRerender } from 'preact/test-utils';
import { setupScratch, teardown, clearOptions, serializeHtml } from '../../../test/_util/helpers';
import { serializeVNode, initDebug } from '../../src/debug';
import * as PropTypes from 'prop-types';
Expand Down Expand Up @@ -392,5 +392,122 @@ describe('debug', () => {
render(<Bar text="foo" />, scratch);
expect(console.error).to.not.be.called;
});

it('should validate propTypes inside lazy()', () => {
const rerender = setupRerender();

function Baz(props) {
return <h1>{props.unhappy}</h1>;
}

Baz.propTypes = {
unhappy: function alwaysThrows(obj, key) { if (obj[key] === 'signal') throw Error('got prop inside lazy()'); }
};


const loader = Promise.resolve({ default: Baz });
const LazyBaz = lazy(() => loader);

render(
<Suspense fallback={<div>fallback...</div>}>
<LazyBaz unhappy="signal" />
</Suspense>,
scratch
);

expect(console.error).to.not.be.called;

return loader.then(() => {
rerender();
expect(errors.length).to.equal(1);
expect(errors[0].includes('got prop')).to.equal(true);
expect(serializeHtml(scratch)).to.equal('<h1>signal</h1>');
});
});

describe('warn for PropTypes on lazy()', () => {
it('should log the function name', () => {
const rerender = setupRerender();

const loader = Promise.resolve({ default: function MyLazyLoadedComponent() { return <div>Hi there</div>; } });
const FakeLazy = lazy(() => loader);
FakeLazy.propTypes = {};
render(
<Suspense fallback={<div>fallback...</div>} >
<FakeLazy />
</Suspense>,
scratch
);

return loader.then(() => {
expect(console.warn).to.be.calledTwice;
expect(warnings[1].includes('MyLazyLoadedComponent')).to.equal(true);
rerender();
expect(console.warn).to.be.calledThrice;
expect(warnings[2].includes('MyLazyLoadedComponent')).to.equal(true);
});
});

it('should log the displayName', () => {
const rerender = setupRerender();

function MyLazyLoadedComponent() { return <div>Hi there</div>; }
MyLazyLoadedComponent.displayName = 'HelloLazy';
const loader = Promise.resolve({ default: MyLazyLoadedComponent });
const FakeLazy = lazy(() => loader);
FakeLazy.propTypes = {};
render(
<Suspense fallback={<div>fallback...</div>} >
<FakeLazy />
</Suspense>,
scratch
);

return loader.then(() => {
expect(console.warn).to.be.calledTwice;
expect(warnings[1].includes('HelloLazy')).to.equal(true);
rerender();
expect(console.warn).to.be.calledThrice;
expect(warnings[2].includes('HelloLazy')).to.equal(true);
});
});

it('should not log a component if lazy throws', () => {
const loader = Promise.reject(new Error('Hey there'));
const FakeLazy = lazy(() => loader);
FakeLazy.propTypes = {};
render(
<Suspense fallback={<div>fallback...</div>} >
<FakeLazy />
</Suspense>,
scratch
);

return loader.catch(() => {
expect(console.warn).to.be.calledOnce;
});
});

it('should not log a component if lazy\'s loader throws', () => {
const FakeLazy = lazy(() => { throw new Error('Hello'); });
FakeLazy.propTypes = {};
let error;
try {
render(
<Suspense fallback={<div>fallback...</div>} >
<FakeLazy />
</Suspense>,
scratch
);
}
catch (e) {
error = e;
}

expect(console.warn).to.be.calledOnce;
expect(error).not.to.be.undefined;
expect(error.message).to.eql('Hello');
});
});
});
});
13 changes: 12 additions & 1 deletion demo/devtools.js
@@ -1,10 +1,16 @@
// eslint-disable-next-line no-unused-vars
import { createElement, Component, memo, Fragment } from "react";
import { createElement, Component, memo, Fragment, Suspense, lazy } from "react";

function Foo() {
return <div>I'm memoed</div>;
}

function LazyComp() {
return <div>I'm (fake) lazy loaded</div>;
}

const Lazy = lazy(() => Promise.resolve({ default: LazyComp }));

const Memoed = memo(Foo);

export default class DevtoolsDemo extends Component {
Expand All @@ -14,6 +20,11 @@ export default class DevtoolsDemo extends Component {
<h1>memo()</h1>
<p><b>functional component:</b></p>
<Memoed />
<h1>lazy()</h1>
<p><b>functional component:</b></p>
<Suspense fallback={<div>Loading (fake) lazy loaded component...</div>}>
<Lazy />
</Suspense>
</div>
);
}
Expand Down
3 changes: 3 additions & 0 deletions demo/index.js
Expand Up @@ -17,6 +17,7 @@ import StyledComp from './styled-components';
import { initDevTools } from 'preact/debug/src/devtools';
import { initDebug } from 'preact/debug/src/debug';
import DevtoolsDemo from './devtools';
import SuspenseDemo from './suspense';

let isBenchmark = /(\/spiral|\/pythagoras|[#&]bench)/g.test(window.location.href);
if (!isBenchmark) {
Expand Down Expand Up @@ -71,6 +72,7 @@ class App extends Component {
<Link href="/people" activeClassName="active">People Browser</Link>
<Link href="/state-order" activeClassName="active">State Order</Link>
<Link href="/styled-components" activeClassName="active">Styled Components</Link>
<Link href="/suspense" activeClassName="active">Suspense / lazy</Link>
</nav>
</header>
<main>
Expand All @@ -96,6 +98,7 @@ class App extends Component {
<KeyBug path="/key_bug" />
<Context path="/context" />
<DevtoolsDemo path="/devtools" />
<SuspenseDemo path="/suspense" />
<EmptyFragment path="/empty-fragment" />
<PeopleBrowser path="/people/:user?" />
<StyledComp path="/styled-components" />
Expand Down
91 changes: 91 additions & 0 deletions demo/suspense.js
@@ -0,0 +1,91 @@
// eslint-disable-next-line no-unused-vars
import { createElement, Component, memo, Fragment, Suspense, lazy } from "react";

function LazyComp() {
return <div>I'm (fake) lazy loaded</div>;
}

const Lazy = lazy(() => Promise.resolve({ default: LazyComp }));

function createSuspension(name, timeout, error) {
let done = false;
let prom;

return {
name,
timeout,
start: () => {
if (!prom) {
prom = new Promise((res, rej) => {
setTimeout(() => {
done = true;
if (error) {
rej(error);
}
else {
res();
}
}, timeout);
});
}

return prom;
},
getPromise: () => prom,
isDone: () => done
};
}

function CustomSuspense({ isDone, start, timeout, name }) {
if (!isDone()) {
throw start();
}

return (
<div>
Hello from CustomSuspense {name}, loaded after {timeout / 1000}s
</div>
);
}

function init() {
return {
s1: createSuspension('1', 1000, null),
s2: createSuspension('2', 2000, null),
s3: createSuspension('3', 3000, null)
};
}

export default class DevtoolsDemo extends Component {
constructor(props) {
super(props);
this.state = init();
this.onRerun = this.onRerun.bind(this);
}

onRerun() {
this.setState(init());
}

render(props, state) {
return (
<div>
<h1>lazy()</h1>
<Suspense fallback={<div>Loading (fake) lazy loaded component...</div>}>
<Lazy />
</Suspense>
<h1>Suspense</h1>
<div>
<button onClick={this.onRerun} >Rerun</button>
</div>
<Suspense fallback={<div>Fallback 1</div>}>
<CustomSuspense {...state.s1} />
<Suspense fallback={<div>Fallback 2</div>}>
<CustomSuspense {...state.s2} />
<CustomSuspense {...state.s3} />
</Suspense>
</Suspense>
</div>
);
}
}
24 changes: 22 additions & 2 deletions src/diff/index.js
Expand Up @@ -43,7 +43,9 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi

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

// Mark dom as empty in case `_children` is any empty array. If it isn't
// we'll set `dom` to the correct value just a few lines later.
Expand Down Expand Up @@ -365,10 +367,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._childDidSuspend) {
component._childDidSuspend(error);
}
else {
continue;
}
}
else if (component.constructor.getDerivedStateFromError!=null) {
component.setState(component.constructor.getDerivedStateFromError(error));
}
else if (component.componentDidCatch!=null) {
Expand All @@ -381,8 +395,14 @@ function catchErrorInComponent(error, component) {
}
catch (e) {
error = e;
isSuspend = false;
}
}
}

if (isSuspend) {
return catchErrorInComponent(new Error('Missing Suspense'), suspendingComponent);
}

throw error;
}
1 change: 1 addition & 0 deletions src/index.js
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';

0 comments on commit a9b8948

Please sign in to comment.