diff --git a/compat/src/suspense.js b/compat/src/suspense.js index 83c6ec699f..7c1fccee6e 100644 --- a/compat/src/suspense.js +++ b/compat/src/suspense.js @@ -1,4 +1,5 @@ import { Component, createElement, options, Fragment } from 'preact'; +import { MODE_HYDRATE } from 'preact/src/constants'; import { assign } from './util'; const oldCatchError = options._catchError; @@ -34,7 +35,7 @@ options.unmount = function (vnode) { // most likely it is because the component is suspended // we set the vnode.type as `null` so that it is not a typeof function // so the unmount will remove the vnode._dom - if (component && vnode._hydrating === true) { + if (component && vnode._flags & MODE_HYDRATE) { vnode.type = null; } @@ -166,8 +167,7 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) { * to remain on screen and hydrate it when the suspense actually gets resolved. * While in non-hydration cases the usual fallback -> component flow would occour. */ - const wasHydrating = suspendingVNode._hydrating === true; - if (!c._pendingSuspensionCount++ && !wasHydrating) { + if (!c._pendingSuspensionCount++ && !(suspendingVNode._flags & MODE_HYDRATE)) { c.setState({ _suspended: (c._detachOnNextRender = c._vnode._children[0]) }); } promise.then(onResolved, onResolved); @@ -204,7 +204,7 @@ Suspense.prototype.render = function (props, state) { /** @type {import('./internal').VNode} */ const fallback = state._suspended && createElement(Fragment, null, props.fallback); - if (fallback) fallback._hydrating = null; + if (fallback) fallback._flags &= ~MODE_HYDRATE; return [ createElement(Fragment, null, state._suspended ? null : props.children), diff --git a/compat/test/browser/hydrate.test.js b/compat/test/browser/hydrate.test.js index e42cdb766f..694def108f 100644 --- a/compat/test/browser/hydrate.test.js +++ b/compat/test/browser/hydrate.test.js @@ -17,10 +17,10 @@ describe('compat hydrate', () => { const input = document.createElement('input'); scratch.appendChild(input); input.focus(); - expect(document.activeElement).to.equal(input); + expect(document.activeElement).to.equalNode(input); hydrate(, scratch); - expect(document.activeElement).to.equal(input); + expect(document.activeElement).to.equalNode(input); }); it('should call the callback', () => { diff --git a/compat/test/browser/suspense-hydration.test.js b/compat/test/browser/suspense-hydration.test.js index b8d45f89f4..9143903238 100644 --- a/compat/test/browser/suspense-hydration.test.js +++ b/compat/test/browser/suspense-hydration.test.js @@ -4,6 +4,7 @@ import React, { hydrate, Fragment, Suspense, + memo, useState } from 'preact/compat'; import { logCall, getLog, clearLog } from '../../../test/_util/logCall'; @@ -13,7 +14,7 @@ import { teardown } from '../../../test/_util/helpers'; import { ul, li, div } from '../../../test/_util/dom'; -import { createLazy } from './suspense-utils'; +import { createLazy, createSuspenseLoader } from './suspense-utils'; /* eslint-env browser, mocha */ describe('suspense hydration', () => { @@ -659,6 +660,45 @@ describe('suspense hydration', () => { }); }); + it('should correctly hydrate and rerender a memoized lazy data loader', () => { + const originalHtml = '

Count: 5

'; + scratch.innerHTML = originalHtml; + + const [useSuspenseLoader, resolve] = createSuspenseLoader(); + + /** @type {() => void} */ + let increment; + const DataLoader = memo(function DataLoader() { + const initialCount = useSuspenseLoader(); + const [count, setCount] = useState(initialCount); + increment = () => setCount(c => c + 1); + + return

Count: {count}

; + }); + + function App() { + return ( + + + + ); + } + + hydrate(, scratch); + rerender(); + expect(scratch.innerHTML).to.equal(originalHtml); + + return resolve(5).then(() => { + rerender(); + expect(scratch.innerHTML).to.equal('

Count: 5

'); + + increment(); + rerender(); + + expect(scratch.innerHTML).to.equal('

Count: 6

'); + }); + }); + // Currently not supported. Hydration doesn't set attributes... but should it // when coming back from suspense if props were updated? it.skip('should hydrate and update attributes with latest props', () => { diff --git a/compat/test/browser/suspense-utils.js b/compat/test/browser/suspense-utils.js index f8f380bed9..c2613d4f5b 100644 --- a/compat/test/browser/suspense-utils.js +++ b/compat/test/browser/suspense-utils.js @@ -114,3 +114,46 @@ export function createSuspender(DefaultComponent) { return [Suspender, suspend]; } + +/** + * @returns {[() => any, (data: any) => Promise, (error: Error) => Promise]} + */ +export function createSuspenseLoader() { + /** @type {(data: any) => Promise} */ + let resolver; + /** @type {(error: Error) => Promise} */ + let rejecter; + /** @type {any} */ + let data = null; + /** @type {Error} */ + let error = null; + + /** @type {Promise} */ + let promise = new Promise((resolve, reject) => { + resolver = result => { + data = result; + resolve(result); + return promise; + }; + + rejecter = e => { + error = e; + reject(e); + return promise; + }; + }); + + function useSuspenseLoader() { + if (error) { + throw error; + } + + if (!data) { + throw promise; + } + + return data; + } + + return [useSuspenseLoader, resolver, rejecter]; +} diff --git a/jsx-runtime/src/index.js b/jsx-runtime/src/index.js index e189ca0b02..8cbe9ea444 100644 --- a/jsx-runtime/src/index.js +++ b/jsx-runtime/src/index.js @@ -2,8 +2,6 @@ import { options, Fragment } from 'preact'; import { encodeEntities } from './utils'; import { IS_NON_DIMENSIONAL } from '../../src/constants'; -/** @typedef {import('preact').VNode} VNode */ - let vnodeId = 0; const isArray = Array.isArray; @@ -43,6 +41,7 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) { } } + /** @type {VNode & { __source: any; __self: any }} */ const vnode = { type, props: normalizedProps, @@ -54,10 +53,10 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) { _dom: null, _nextDom: undefined, _component: null, - _hydrating: null, constructor: undefined, _original: --vnodeId, _index: -1, + _flags: 0, __source, __self }; diff --git a/mangle.json b/mangle.json index 76e9fb5956..7744675ce2 100644 --- a/mangle.json +++ b/mangle.json @@ -52,9 +52,9 @@ "$_onResolve": "__R", "$_suspended": "__a", "$_dom": "__e", - "$_hydrating": "__h", "$_component": "__c", "$_index": "__i", + "$_flags": "__u", "$__html": "__html", "$_parent": "__", "$_pendingError": "__E", diff --git a/src/component.js b/src/component.js index 73c5afd2e4..d1e0d27579 100644 --- a/src/component.js +++ b/src/component.js @@ -2,6 +2,7 @@ import { assign } from './util'; import { diff, commitRoot } from './diff/index'; import options from './options'; import { Fragment } from './create-element'; +import { MODE_HYDRATE } from './constants'; /** * Base Component class. Provides `setState()` and `forceUpdate()`, which @@ -122,12 +123,11 @@ export function getDomSibling(vnode, childIndex) { function renderComponent(component) { let oldVNode = component._vnode, oldDom = oldVNode._dom, - parentDom = component._parentDom; + parentDom = component._parentDom, + commitQueue = [], + refQueue = []; if (parentDom) { - let commitQueue = [], - refQueue = []; - const newVNode = assign({}, oldVNode); newVNode._original = oldVNode._original + 1; @@ -137,10 +137,10 @@ function renderComponent(component) { oldVNode, component._globalContext, parentDom.ownerSVGElement !== undefined, - oldVNode._hydrating != null ? [oldDom] : null, + oldVNode._flags & MODE_HYDRATE ? [oldDom] : null, commitQueue, oldDom == null ? getDomSibling(oldVNode) : oldDom, - oldVNode._hydrating, + !!(oldVNode._flags & MODE_HYDRATE), refQueue ); diff --git a/src/constants.js b/src/constants.js index a8fc694027..3bcec6cfac 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,3 +1,15 @@ +/** Normal hydration that attaches to a DOM tree but does not diff it. */ +export const MODE_HYDRATE = 1 << 5; +/** Signifies this VNode suspended on the previous render */ +export const MODE_SUSPENDED = 1 << 7; +/** Indicates that this node needs to be inserted while patching children */ +export const INSERT_VNODE = 1 << 16; +/** Indicates a VNode has been matched with another VNode in the diff */ +export const MATCHED = 1 << 17; + +/** Reset all mode flags */ +export const RESET_MODE = ~(MODE_HYDRATE | MODE_SUSPENDED); + export const EMPTY_OBJ = /** @type {any} */ ({}); export const EMPTY_ARR = []; export const IS_NON_DIMENSIONAL = diff --git a/src/create-element.js b/src/create-element.js index 239b7efdd7..66898b2224 100644 --- a/src/create-element.js +++ b/src/create-element.js @@ -72,10 +72,10 @@ export function createVNode(type, props, key, ref, original) { // a _nextDom that has been set to `null` _nextDom: undefined, _component: null, - _hydrating: null, constructor: undefined, _original: original == null ? ++vnodeId : original, - _index: -1 + _index: -1, + _flags: 0 }; // Only invoke the vnode hook if this was *not* a direct copy: diff --git a/src/diff/children.js b/src/diff/children.js index 251c6b63d2..0d3b704b51 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -1,6 +1,6 @@ import { diff, unmount, applyRef } from './index'; import { createVNode, Fragment } from '../create-element'; -import { EMPTY_OBJ, EMPTY_ARR } from '../constants'; +import { EMPTY_OBJ, EMPTY_ARR, INSERT_VNODE, MATCHED } from '../constants'; import { isArray } from '../util'; import { getDomSibling } from '../component'; @@ -40,7 +40,6 @@ export function diffChildren( refQueue ) { let i, - j, /** @type {VNode} */ oldVNode, /** @type {VNode} */ @@ -48,17 +47,132 @@ export function diffChildren( /** @type {PreactElement} */ newDom, /** @type {PreactElement} */ - firstChildDom, - skew = 0; + firstChildDom; // This is a compression of oldParentVNode!=null && oldParentVNode != EMPTY_OBJ && oldParentVNode._children || EMPTY_ARR // as EMPTY_OBJ._children should be `undefined`. /** @type {VNode[]} */ let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR; + let newChildrenLength = renderResult.length; + + newParentVNode._nextDom = oldDom; + constructNewChildrenArray(newParentVNode, renderResult, oldChildren); + oldDom = newParentVNode._nextDom; + + for (i = 0; i < newChildrenLength; i++) { + childVNode = newParentVNode._children[i]; + + if ( + childVNode == null || + typeof childVNode == 'boolean' || + typeof childVNode == 'function' + ) { + continue; + } + + // At this point, constructNewChildrenArray has assigned _index to be the + // matchingIndex for this VNode's oldVNode (or -1 if there is no oldVNode). + if (childVNode._index === -1) { + oldVNode = EMPTY_OBJ; + } else { + oldVNode = oldChildren[childVNode._index] || EMPTY_OBJ; + } + + // Update childVNode._index to its final index + childVNode._index = i; + + // Morph the old element into the new one, but don't append it to the dom yet + diff( + parentDom, + childVNode, + oldVNode, + globalContext, + isSvg, + excessDomChildren, + commitQueue, + oldDom, + isHydrating, + refQueue + ); + + // Adjust DOM nodes + newDom = childVNode._dom; + if (childVNode.ref && oldVNode.ref != childVNode.ref) { + if (oldVNode.ref) { + applyRef(oldVNode.ref, null, childVNode); + } + refQueue.push( + childVNode.ref, + childVNode._component || newDom, + childVNode + ); + } + + if (firstChildDom == null && newDom != null) { + firstChildDom = newDom; + } + + if ( + childVNode._flags & INSERT_VNODE || + oldVNode._children === childVNode._children + ) { + oldDom = insert(childVNode, oldDom, parentDom); + } else if ( + typeof childVNode.type == 'function' && + childVNode._nextDom !== undefined + ) { + // Since Fragments or components that return Fragment like VNodes can + // contain multiple DOM nodes as the same level, continue the diff from + // the sibling of last DOM child of this child VNode + oldDom = childVNode._nextDom; + } else if (newDom) { + oldDom = newDom.nextSibling; + } + + // Eagerly cleanup _nextDom. We don't need to persist the value because it + // is only used by `diffChildren` to determine where to resume the diff + // after diffing Components and Fragments. Once we store it the nextDOM + // local var, we can clean up the property. Also prevents us hanging on to + // DOM nodes that may have been unmounted. + childVNode._nextDom = undefined; + + // Unset diffing flags + childVNode._flags &= ~(INSERT_VNODE | MATCHED); + } + + // TODO: With new child diffing algo, consider alt ways to diff Fragments. + // Such as dropping oldDom and moving fragments in place + // + // Because the newParentVNode is Fragment-like, we need to set it's + // _nextDom property to the nextSibling of its last child DOM node. + // + // `oldDom` contains the correct value here because if the last child + // is a Fragment-like, then oldDom has already been set to that child's _nextDom. + // If the last child is a DOM VNode, then oldDom will be set to that DOM + // node's nextSibling. + newParentVNode._nextDom = oldDom; + newParentVNode._dom = firstChildDom; +} + +/** + * @param {VNode} newParentVNode + * @param {ComponentChildren[]} renderResult + * @param {VNode[]} oldChildren + */ +function constructNewChildrenArray(newParentVNode, renderResult, oldChildren) { + /** @type {number} */ + let i; + /** @type {VNode} */ + let childVNode; + /** @type {VNode} */ + let oldVNode; + + const newChildrenLength = renderResult.length; let oldChildrenLength = oldChildren.length, - remainingOldChildren = oldChildrenLength, - newChildrenLength = renderResult.length; + remainingOldChildren = oldChildrenLength; + + let skew = 0; newParentVNode._children = []; for (i = 0; i < newChildrenLength; i++) { @@ -113,24 +227,27 @@ export function diffChildren( childVNode = newParentVNode._children[i] = childVNode; } - // Terser removes the `continue` here and wraps the loop body - // in a `if (childVNode) { ... } condition + // Handle unmounting null placeholders, i.e. VNode => null in unkeyed children if (childVNode == null) { oldVNode = oldChildren[i]; if (oldVNode && oldVNode.key == null && oldVNode._dom) { - if (oldVNode._dom == oldDom) { - oldDom = getDomSibling(oldVNode); - - if (typeof newParentVNode.type == 'function') { - // If the parent VNode is a component/fragment, make sure its diff - // continues with a DOM node that is still mounted in case this loop - // exits here because the rest of the new children are `null`. - newParentVNode._nextDom = oldDom; - } + if (oldVNode._dom == newParentVNode._nextDom) { + newParentVNode._nextDom = getDomSibling(oldVNode); } unmount(oldVNode, oldVNode, false); + + // Explicitly nullify this position in oldChildren instead of just + // setting `_match=true` to prevent other routines (e.g. + // `findMatchingIndex` or `getDomSibling`) from thinking VNodes or DOM + // nodes in this position are still available to be used in diffing when + // they have actually already been unmounted. For example, by only + // setting `_match=true` here, the unmounting loop later would attempt + // to unmount this VNode again seeing `_match==true`. Further, + // getDomSibling doesn't know about _match and so would incorrectly + // assume DOM nodes in this subtree are mounted and usable. oldChildren[i] = null; + remainingOldChildren--; } continue; @@ -138,9 +255,8 @@ export function diffChildren( childVNode._parent = newParentVNode; childVNode._depth = newParentVNode._depth + 1; - childVNode._index = i; - let skewedIndex = i + skew; + const skewedIndex = i + skew; const matchingIndex = findMatchingIndex( childVNode, oldChildren, @@ -148,41 +264,26 @@ export function diffChildren( remainingOldChildren ); - if (matchingIndex === -1) { - oldVNode = EMPTY_OBJ; - } else { - oldVNode = oldChildren[matchingIndex] || EMPTY_OBJ; - oldChildren[matchingIndex] = undefined; - remainingOldChildren--; - } - - // Morph the old element into the new one, but don't append it to the dom yet - diff( - parentDom, - childVNode, - oldVNode, - globalContext, - isSvg, - excessDomChildren, - commitQueue, - oldDom, - isHydrating, - refQueue - ); + // Temporarily store the matchingIndex on the _index property so we can pull + // out the oldVNode in diffChildren. We'll override this to the VNode's + // final index after using this property to get the oldVNode + childVNode._index = matchingIndex; - newDom = childVNode._dom; - if ((j = childVNode.ref) && oldVNode.ref != j) { - if (oldVNode.ref) { - applyRef(oldVNode.ref, null, childVNode); + if (matchingIndex !== -1) { + remainingOldChildren--; + if (oldChildren[matchingIndex]) { + oldChildren[matchingIndex]._flags |= MATCHED; } - refQueue.push(j, childVNode._component || newDom, childVNode); } - if (firstChildDom == null && newDom != null) { - firstChildDom = newDom; - } + // Here, we define isMounting for the purposes of the skew diffing + // algorithm. Nodes that are unsuspending are considered mounting and we detect + // this by checking if oldVNode._original === null + const isMounting = + matchingIndex === -1 || + oldChildren[matchingIndex] == null || + oldChildren[matchingIndex]._original === null; - let isMounting = oldVNode === EMPTY_OBJ || oldVNode._original === null; if (isMounting) { if (matchingIndex == -1) { skew--; @@ -208,98 +309,61 @@ export function diffChildren( } } - skewedIndex = i + skew; - - if (typeof childVNode.type == 'function') { - if ( - matchingIndex !== skewedIndex || - oldVNode._children === childVNode._children - ) { - oldDom = reorderChildren(childVNode, oldDom, parentDom); - } else if (childVNode._nextDom !== undefined) { - // Only Fragments or components that return Fragment like VNodes will - // have a non-undefined _nextDom. Continue the diff from the sibling - // of last DOM child of this child VNode - oldDom = childVNode._nextDom; - } else if (newDom) { - oldDom = newDom.nextSibling; - } - - // Eagerly cleanup _nextDom. We don't need to persist the value because - // it is only used by `diffChildren` to determine where to resume the diff after - // diffing Components and Fragments. Once we store it the nextDOM local var, we - // can clean up the property - childVNode._nextDom = undefined; - } else if (newDom) { - if (matchingIndex !== skewedIndex || isMounting) { - oldDom = placeChild(parentDom, newDom, oldDom); - } else { - oldDom = newDom.nextSibling; - } - } - - if (typeof newParentVNode.type == 'function') { - // Because the newParentVNode is Fragment-like, we need to set it's - // _nextDom property to the nextSibling of its last child DOM node. - // - // `oldDom` contains the correct value here because if the last child - // is a Fragment-like, then oldDom has already been set to that child's _nextDom. - // If the last child is a DOM VNode, then oldDom will be set to that DOM - // node's nextSibling. - newParentVNode._nextDom = oldDom; + // Move this VNode's DOM if the original index (matchingIndex) doesn't match + // the new skew index (i + new skew) or it's a mounting DOM VNode + if ( + matchingIndex !== i + skew || + (typeof childVNode.type != 'function' && isMounting) + ) { + childVNode._flags |= INSERT_VNODE; } } - newParentVNode._dom = firstChildDom; - - // Remove remaining oldChildren if there are any. - for (i = oldChildrenLength; i--; ) { - if (oldChildren[i] != null) { - if ( - typeof newParentVNode.type == 'function' && - oldChildren[i]._dom != null && - oldChildren[i]._dom == oldDom - ) { - // If oldDom points to a dom node that is about to be unmounted, then - // get the next sibling of that vnode and set _nextDom to it, so the - // parent's diff continues diffing an existing DOM node - newParentVNode._nextDom = oldChildren[i]._dom.nextSibling; + // Remove remaining oldChildren if there are any. Loop forwards so that as we + // unmount DOM from the beginning of the oldChildren, we can adjust oldDom to + // point to the next child, which needs to be the first DOM node that won't be + // unmounted. + for (i = 0; i < oldChildrenLength; i++) { + oldVNode = oldChildren[i]; + if (oldVNode != null && (oldVNode._flags & MATCHED) === 0) { + if (oldVNode._dom == newParentVNode._nextDom) { + newParentVNode._nextDom = getDomSibling(oldVNode); } - unmount(oldChildren[i], oldChildren[i]); + unmount(oldVNode, oldVNode); } } } /** - * @param {VNode} childVNode + * @param {VNode} parentVNode * @param {PreactElement} oldDom * @param {PreactElement} parentDom * @returns {PreactElement} */ -function reorderChildren(childVNode, oldDom, parentDom) { +function insert(parentVNode, oldDom, parentDom) { // Note: VNodes in nested suspended trees may be missing _children. - let c = childVNode._children; - - let tmp = 0; - for (; c && tmp < c.length; tmp++) { - let vnode = c[tmp]; - if (vnode) { - // We typically enter this code path on sCU bailout, where we copy - // oldVNode._children to newVNode._children. If that is the case, we need - // to update the old children's _parent pointer to point to the newVNode - // (childVNode here). - vnode._parent = childVNode; - - if (typeof vnode.type == 'function') { - oldDom = reorderChildren(vnode, oldDom, parentDom); - } else { - oldDom = placeChild(parentDom, vnode._dom, oldDom); + + if (typeof parentVNode.type == 'function') { + let children = parentVNode._children; + for (let i = 0; children && i < children.length; i++) { + if (children[i]) { + // If we enter this code path on sCU bailout, where we copy + // oldVNode._children to newVNode._children, we need to update the old + // children's _parent pointer to point to the newVNode (parentVNode + // here). + children[i]._parent = parentVNode; + oldDom = insert(children[i], oldDom, parentDom); } } + + return oldDom; + } else if (parentVNode._dom != oldDom) { + parentDom.insertBefore(parentVNode._dom, oldDom || null); + oldDom = parentVNode._dom; } - return oldDom; + return oldDom && oldDom.nextSibling; } /** @@ -321,20 +385,6 @@ export function toChildArray(children, out) { return out; } -/** - * @param {PreactElement} parentDom - * @param {PreactElement} newDom - * @param {PreactElement} oldDom - * @returns {PreactElement} - */ -function placeChild(parentDom, newDom, oldDom) { - if (newDom != oldDom) { - parentDom.insertBefore(newDom, oldDom || null); - } - - return newDom.nextSibling; -} - /** * @param {VNode} childVNode * @param {VNode[]} oldChildren @@ -354,16 +404,33 @@ function findMatchingIndex( let y = skewedIndex + 1; let oldVNode = oldChildren[skewedIndex]; + // We only need to perform a search if there are more children + // (remainingOldChildren) to search. However, if the oldVNode we just looked + // at skewedIndex was not already used in this diff, then there must be at + // least 1 other (so greater than 1) remainingOldChildren to attempt to match + // against. So the following condition checks that ensuring + // remainingOldChildren > 1 if the oldVNode is not already used/matched. Else + // if the oldVNode was null or matched, then there could needs to be at least + // 1 (aka `remainingOldChildren > 0`) children to find and compare against. + let shouldSearch = + remainingOldChildren > + (oldVNode != null && (oldVNode._flags & MATCHED) === 0 ? 1 : 0); + if ( oldVNode === null || (oldVNode && key == oldVNode.key && type === oldVNode.type) ) { return skewedIndex; - } else if (remainingOldChildren > (oldVNode != null ? 1 : 0)) { + } else if (shouldSearch) { while (x >= 0 || y < oldChildren.length) { if (x >= 0) { oldVNode = oldChildren[x]; - if (oldVNode && key == oldVNode.key && type === oldVNode.type) { + if ( + oldVNode && + (oldVNode._flags & MATCHED) === 0 && + key == oldVNode.key && + type === oldVNode.type + ) { return x; } x--; @@ -371,7 +438,12 @@ function findMatchingIndex( if (y < oldChildren.length) { oldVNode = oldChildren[y]; - if (oldVNode && key == oldVNode.key && type === oldVNode.type) { + if ( + oldVNode && + (oldVNode._flags & MATCHED) === 0 && + key == oldVNode.key && + type === oldVNode.type + ) { return y; } y++; diff --git a/src/diff/index.js b/src/diff/index.js index 17254a32f6..515f1b40ab 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -1,4 +1,9 @@ -import { EMPTY_OBJ } from '../constants'; +import { + EMPTY_OBJ, + MODE_HYDRATE, + MODE_SUSPENDED, + RESET_MODE +} from '../constants'; import { BaseComponent, getDomSibling } from '../component'; import { Fragment } from '../create-element'; import { diffChildren } from './children'; @@ -45,11 +50,9 @@ export function diff( if (newVNode.constructor !== undefined) return null; // If the previous diff bailed out, resume creating/hydrating. - if (oldVNode._hydrating != null) { - isHydrating = oldVNode._hydrating; + if (oldVNode._flags & MODE_SUSPENDED) { + isHydrating = !!(oldVNode._flags & MODE_HYDRATE); oldDom = newVNode._dom = oldVNode._dom; - // if we resume, we want the tree to be "unlocked" - newVNode._hydrating = null; excessDomChildren = [oldDom]; } @@ -253,7 +256,7 @@ export function diff( c.base = newVNode._dom; // We successfully rendered this VNode, unset any stored hydration/bailout state: - newVNode._hydrating = null; + newVNode._flags &= RESET_MODE; if (c._renderCallbacks.length) { commitQueue.push(c); @@ -267,7 +270,9 @@ export function diff( // if hydrating or creating initial tree, bailout preserves DOM: if (isHydrating || excessDomChildren != null) { newVNode._dom = oldDom; - newVNode._hydrating = !!isHydrating; + newVNode._flags |= isHydrating + ? MODE_HYDRATE | MODE_SUSPENDED + : MODE_HYDRATE; excessDomChildren[excessDomChildren.indexOf(oldDom)] = null; // ^ could possibly be simplified to: // excessDomChildren.length = 0; diff --git a/src/internal.d.ts b/src/internal.d.ts index ea56157b1e..fda7ce89fd 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -147,10 +147,10 @@ declare global { */ _nextDom: PreactElement | null | undefined; _component: Component | null; - _hydrating: boolean | null; constructor: undefined; _original: number; _index: number; + _flags: number; } export interface Component

extends preact.Component { diff --git a/test/browser/createContext.test.js b/test/browser/createContext.test.js index f4610ed372..b27f20b625 100644 --- a/test/browser/createContext.test.js +++ b/test/browser/createContext.test.js @@ -877,8 +877,8 @@ describe('createContext', () => { expect(events).to.deep.equal([ 'render 0', 'mount 0', - 'render 1', 'unmount 0', + 'render 1', 'mount 1' ]); }); diff --git a/test/browser/fragments.test.js b/test/browser/fragments.test.js index 3e5292f2c5..20fed865e4 100644 --- a/test/browser/fragments.test.js +++ b/test/browser/fragments.test.js @@ -1,7 +1,7 @@ import { setupRerender } from 'preact/test-utils'; import { createElement, render, Component, Fragment } from 'preact'; import { setupScratch, teardown } from '../_util/helpers'; -import { span, div, ul, ol, li, section, p } from '../_util/dom'; +import { span, div, ul, ol, li, section } from '../_util/dom'; import { logCall, clearLog, getLog } from '../_util/logCall'; /** @jsx createElement */ @@ -235,9 +235,9 @@ describe('Fragment', () => { expect(scratch.innerHTML).to.equal(div([div(1), span(2), span(2)])); expectDomLogToBe([ + '1.remove()', '

.appendChild(#text)', - '
122.insertBefore(
1, 1)', - '1.remove()' + '
22.insertBefore(
1, 2)' ]); }); @@ -357,9 +357,9 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
Hello
'); expectDomLogToBe([ + '
Hello.remove()', '
.appendChild(#text)', - '
Hello.insertBefore(
Hello,
Hello)', - '
Hello.remove()' + '
.appendChild(
Hello)' ]); clearLog(); @@ -368,10 +368,10 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
Hello
'); expectDomLogToBe([ + '
Hello.remove()', '
.appendChild(#text)', // Re-append the Stateful DOM since it has been re-parented - '
Hello.insertBefore(
Hello,
Hello)', - '
Hello.remove()' + '
.appendChild(
Hello)' ]); }); @@ -396,9 +396,9 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
Hello
'); expectDomLogToBe([ + '
Hello.remove()', '
.appendChild(#text)', - '
Hello.insertBefore(
Hello,
Hello)', - '
Hello.remove()' + '
.appendChild(
Hello)' ]); clearLog(); @@ -407,9 +407,9 @@ describe('Fragment', () => { expect(ops).to.deep.equal([]); expect(scratch.innerHTML).to.equal('
Hello
'); expectDomLogToBe([ + '
Hello.remove()', '
.appendChild(#text)', - '
Hello.insertBefore(
Hello,
Hello)', - '
Hello.remove()' + '
.appendChild(
Hello)' ]); }); @@ -647,7 +647,6 @@ describe('Fragment', () => { }); it('should preserve order for fragment switching', () => { - /** @type {(newState: { isLoading: boolean; data: number | null }) => void} */ let set; class Foo extends Component { constructor(props) { @@ -678,84 +677,6 @@ describe('Fragment', () => { ); }); - it('should preserve order for fragment switching with sibling DOM', () => { - /** @type {(newState: { isLoading: boolean; data: number | null }) => void} */ - let set; - class Foo extends Component { - constructor(props) { - super(props); - this.state = { isLoading: true, data: null }; - set = this.setState.bind(this); - } - render(props, { isLoading, data }) { - return ( - -
HEADER
- {isLoading ?
Loading...
: null} - {data ?
Content: {data}
: null} -
FOOTER
-
- ); - } - } - - render(, scratch); - expect(scratch.innerHTML).to.equal( - '
HEADER
Loading...
FOOTER
' - ); - - set({ isLoading: false, data: 2 }); - rerender(); - expect(scratch.innerHTML).to.equal( - '
HEADER
Content: 2
FOOTER
' - ); - }); - - it('should preserve order for fragment switching with sibling Components', () => { - /** @type {(newState: { isLoading: boolean; data: number | null }) => void} */ - let set; - class Foo extends Component { - constructor(props) { - super(props); - this.state = { isLoading: true, data: null }; - set = this.setState.bind(this); - } - render(props, { isLoading, data }) { - return ( - -
HEADER
- {isLoading ?
Loading...
: null} - {data ?
Content: {data}
: null} -
- ); - } - } - - function Footer() { - return
FOOTER
; - } - - function App() { - return ( - - -