Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gathering @developit's suspense hydration work #2259

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
04c1d22
assembilg all of @developit's work
prateekbh Jan 17, 2020
dab21aa
addressing comments
prateekbh Jan 17, 2020
422bc72
adding test changes
prateekbh Jan 17, 2020
a40e488
Merge branch 'master' of https://github.com/preactjs/preact into prat…
prateekbh Jan 17, 2020
3ecbb03
addressing comments
prateekbh Jan 17, 2020
fec4986
adding suspense hydration tests
prateekbh Jan 18, 2020
81291f6
fixing hydration fallback bug
prateekbh Jan 21, 2020
fae5a0d
Merge branch 'master' into prateekbh/suspense-hydration
prateekbh Jan 21, 2020
74e7934
removing _hydrating flag
prateekbh Jan 22, 2020
ae8c8bc
Merge branch 'prateekbh/suspense-hydration' of https://github.com/pre…
prateekbh Jan 22, 2020
573dbbd
passing vnode instead of boolean
prateekbh Jan 22, 2020
a0833e4
fixing jsdocs
prateekbh Jan 22, 2020
35beddd
Merge branch 'master' into prateekbh/suspense-hydration
prateekbh Jan 22, 2020
986f899
Merge branch 'master' into prateekbh/suspense-hydration
marvinhagemeister Jan 23, 2020
1226ac4
Merge branch 'master' of https://github.com/preactjs/preact into prat…
prateekbh Jan 23, 2020
7ce494f
Merge branch 'master' into prateekbh/suspense-hydration
developit Jan 24, 2020
5ba7ef2
taking the common condition out
prateekbh Jan 24, 2020
a26caf1
Merge branch 'prateekbh/suspense-hydration' of https://github.com/pre…
prateekbh Jan 24, 2020
0a87ce7
Merge branch 'master' into prateekbh/suspense-hydration
prateekbh Jan 24, 2020
eca2cf2
Merge branch 'master' into prateekbh/suspense-hydration
developit Jan 27, 2020
8f679f0
Merge branch 'master' into prateekbh/suspense-hydration
reznord Jan 27, 2020
7f844b1
Merge branch 'master' into prateekbh/suspense-hydration
andrewiggins Feb 1, 2020
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
3 changes: 2 additions & 1 deletion compat/src/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export interface Component<P = {}, S = {}> extends PreactComponent<P, S> {

_childDidSuspend?(
error: Promise<void>,
suspendingComponent: Component<any, any>
suspendingComponent: Component<any, any>,
oldVNode?: VNode
): void;
_suspendedComponentWillUnmount?(): void;
}
Expand Down
16 changes: 13 additions & 3 deletions compat/src/suspense.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ options._catchError = function(error, newVNode, oldVNode) {
for (; (vnode = vnode._parent); ) {
if ((component = vnode._component) && component._childDidSuspend) {
// Don't call oldCatchError if we found a Suspense
return component._childDidSuspend(error, newVNode._component);
return component._childDidSuspend(error, newVNode._component, oldVNode);
}
}
}
Expand Down Expand Up @@ -42,8 +42,13 @@ Suspense.prototype = new Component();
/**
* @param {Promise} promise The thrown promise
* @param {Component<any, any>} suspendingComponent The suspending component
* @param {import('./internal').VNode | null | undefined} oldVNode old VNode caught in the _catchError options
*/
Suspense.prototype._childDidSuspend = function(promise, suspendingComponent) {
Suspense.prototype._childDidSuspend = function(
promise,
suspendingComponent,
oldVNode
) {
/** @type {import('./internal').SuspenseComponent} */
const c = this;

Expand Down Expand Up @@ -79,7 +84,12 @@ Suspense.prototype._childDidSuspend = function(promise, suspendingComponent) {
}
};

if (!c._suspensions++) {
/**
* We do not set `suspended: true` during hydration because we want the actual markup to remain on screen
* and hydrate it when the suspense actually gets resolved.
* While in non-hydration cases the usual fallbac -> component flow would occour.
*/
if (!(oldVNode && oldVNode._hydrateDom) && !c._suspensions++) {
c.setState({ _suspended: (c._detachOnNextRender = c._vnode._children[0]) });
}
promise.then(onResolved, onResolved);
Expand Down
1 change: 1 addition & 0 deletions mangle.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"$_children": "__k",
"$_suspensions": "__u",
"$_dom": "__e",
"$_hydrateDom": "__s",
"$_component": "__c",
"$__html": "__html",
"$_parent": "__",
Expand Down
1 change: 1 addition & 0 deletions src/create-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function createVNode(type, props, key, ref) {
_dom: null,
_lastDomChild: null,
_component: null,
_hydrateDom: null,
constructor: undefined
};

Expand Down
4 changes: 4 additions & 0 deletions src/diff/children.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ export function diffChildren(
firstChildDom = newDom;
}

if (excessDomChildren != null) {
excessDomChildren[excessDomChildren.indexOf(newDom)] = null;
marvinhagemeister marked this conversation as resolved.
Show resolved Hide resolved
}

if (childVNode._lastDomChild != null) {
// Only Fragments or components that return Fragment like VNodes will
// have a non-null _lastDomChild. Continue the diff from the end of
Expand Down
17 changes: 17 additions & 0 deletions src/diff/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export function diff(
// constructor as undefined. This to prevent JSON-injection.
if (newVNode.constructor !== undefined) return null;

// If this was the innermost VNode at a point where the tree suspended,
// pick up diffing where we left off using the saved DOM element and hydration state.
if (oldVNode._hydrateDom && excessDomChildren == null) {
newVNode._dom = oldDom = oldVNode._hydrateDom;
excessDomChildren = [oldDom];
oldVNode._hydrateDom = null;
}

if ((tmp = options._diff)) tmp(newVNode);

try {
Expand Down Expand Up @@ -215,6 +223,11 @@ export function diff(

if ((tmp = options.diffed)) tmp(newVNode);
} catch (e) {
if (isHydrating) {
// Before bailing out, mark the current VNode with the DOM element and hydration state.
// We can use this information if we return here to render later on.
oldVNode._hydrateDom = newVNode._dom = oldDom;
}
options._catchError(e, newVNode, oldVNode);
}

Expand Down Expand Up @@ -291,6 +304,10 @@ function diffElementNodes(
}

if (dom == null) {
if (excessDomChildren != null) {
excessDomChildren[excessDomChildren.indexOf(dom)] = null;
}

if (newVNode.type === null) {
return document.createTextNode(newProps);
}
Expand Down
46 changes: 46 additions & 0 deletions test/browser/hydrate.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { createElement, hydrate, Fragment } from 'preact';
import { Suspense } from 'preact/compat';
import { useState } from 'preact/hooks';
import {
setupScratch,
teardown,
Expand All @@ -7,11 +9,13 @@ import {
} from '../_util/helpers';
import { ul, li, div } from '../_util/dom';
import { logCall, clearLog, getLog } from '../_util/logCall';
import { setupRerender } from 'preact/test-utils';

/** @jsx createElement */

describe('hydrate()', () => {
let scratch;
let rerender;

const List = ({ children }) => <ul>{children}</ul>;
const ListItem = ({ children }) => <li>{children}</li>;
Expand All @@ -27,6 +31,7 @@ describe('hydrate()', () => {

beforeEach(() => {
scratch = setupScratch();
rerender = setupRerender();
});

afterEach(() => {
Expand Down Expand Up @@ -276,4 +281,45 @@ describe('hydrate()', () => {
hydrate(<Component foo="bar" />, element);
expect(element.innerHTML).to.equal('<p>hello bar</p>');
});

it('should reuse suspense component markup when suspense resolves during hydration', () => {
scratch.innerHTML = '<div id="test"><p>hello bar</p><p>Hello foo</p></div>';
const element = scratch;
let resolver;
const Component = () => {
const [state, setState] = useState(false);
if (!state) {
throw new Promise(resolve => {
resolver = () => {
setState(true);
resolve();
};
});
} else {
return <p>hello bar</p>;
}
};
const App = () => {
return (
<div id="test">
<Suspense fallback={<div>baz</div>}>
<Component />
</Suspense>
<p>Hello foo</p>
</div>
);
};
hydrate(<App />, element);
rerender();
expect(element.innerHTML).to.equal(
'<div id="test"><p>hello bar</p><p>Hello foo</p></div>'
);
const removeChildSpy = sinon.spy(element.firstChild, 'removeChild');
resolver();
rerender();
expect(removeChildSpy).to.be.not.called;
expect(element.innerHTML).to.equal(
'<div id="test"><p>hello bar</p><p>Hello foo</p></div>'
);
});
});
10 changes: 7 additions & 3 deletions test/browser/render.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1271,21 +1271,25 @@ describe('render()', () => {
scratch.appendChild(container);
render(<div>Hello</div>, scratch, scratch.firstElementChild);
expect(scratch.innerHTML).to.equal('<div>Hello</div>');
const child = scratch.firstElementChild;

render(<div>Hello</div>, scratch, scratch.firstElementChild);
expect(scratch.innerHTML).to.equal('<div>Hello</div>');
expect(scratch.firstElementChild).to.equal(child);
});

it('should replaceNode after rendering', () => {
function App({ i }) {
return <p>{i}</p>;
}

scratch.innerHTML = '';
render(<App i={2} />, scratch);
expect(scratch.innerHTML).to.equal('<p>2</p>');
const child = scratch.firstElementChild;

render(<App i={3} />, scratch, scratch.firstChild);
render(<App i={3} />, scratch, child);
expect(scratch.innerHTML).to.equal('<p>3</p>');
expect(scratch.firstElementChild).to.equal(child);
});

it('should not cause infinite loop with referentially equal props', () => {
Expand Down Expand Up @@ -1325,7 +1329,7 @@ describe('render()', () => {
it('should use replaceNode as render root and not inject into it', () => {
const childA = scratch.querySelector('#a');
render(<div id="a">contents</div>, scratch, childA);
expect(scratch.querySelector('#a')).to.equalNode(childA);
expect(scratch.querySelector('#a')).to.equal(childA);
expect(childA.innerHTML).to.equal('contents');
});

Expand Down