diff --git a/lib/renderer.js b/lib/renderer.js index 5a11034..0a32b5e 100644 --- a/lib/renderer.js +++ b/lib/renderer.js @@ -47,6 +47,7 @@ class PartialRenderer extends ReactDOMServerRenderer { // Init lazy node awaiting counter this.numAwaiting = 0; + this.fallbacksQueue = []; // Init interrupt signal this.interrupt = null; @@ -88,10 +89,7 @@ class PartialRenderer extends ReactDOMServerRenderer { this.errored(err); } - if (this.numAwaiting !== 0) return; - - const {suspenseNode} = this; - if (suspenseNode && suspenseNode.suspended) return; + if (this.numAwaiting !== 0 || this.fallbacksQueue.length > 0) return; // Finished processing this.destroy(); @@ -116,7 +114,22 @@ class PartialRenderer extends ReactDOMServerRenderer { errored(err) { // Abort all promises - this.abortDescendents(this.tree); + walkTree(this.tree, node => { + const {type} = node; + if (type === TYPE_TEXT) return false; + + if (type === TYPE_PROMISE && !node.resolved) { + node.resolved = true; + abort(node.promise); + } + + return true; + }); + + this.numAwaiting = 0; + + // Clear fallbacks queue + this.fallbacksQueue.length = 0; // Record error this.error = err; @@ -276,6 +289,7 @@ class PartialRenderer extends ReactDOMServerRenderer { node.suspendedAbove = parentSuspense ? parentSuspense.suspended || parentSuspense.suspendedAbove : false; + node.containsLazy = false; this.suspenseNode = node; this.node = node; @@ -302,6 +316,9 @@ class PartialRenderer extends ReactDOMServerRenderer { const {stack} = this; const frame = this.stackPopOriginal.call(stack); + // Flag suspense as containing lazy + if (!suspenseNode.containsLazy) suspenseNode.containsLazy = true; + // If above suspense boundary suspended, suspend this too if (suspenseNode.suspendedAbove && !suspenseNode.suspended) suspenseNode.suspended = true; @@ -384,8 +401,7 @@ class PartialRenderer extends ReactDOMServerRenderer { rerender(node, element) { // Step into node and reinstate stack state with element on stack ready to render this.node = node; - const suspenseNode = node.parentSuspense; - this.suspenseNode = suspenseNode; + this.suspenseNode = node.parentSuspense; this.restoreStack(node.stackState, element); // Render element @@ -397,18 +413,25 @@ class PartialRenderer extends ReactDOMServerRenderer { // Clear contexts added in `.restoreStack()` this.resetProviders(); - // If suspended, render fallback of suspense boundary - if (!suspenseNode || !suspenseNode.suspended) return; - - // Convert node to fallback and discard children - const {fallback} = suspenseNode; - this.convertNodeToFallback(suspenseNode); + // Process fallbacks queue. + // Convert nodes to fallbacks + // Find first fallback that requires rendering, and render it. + const {fallbacksQueue} = this; + let suspenseNode, fallback; + while (true) { // eslint-disable-line no-constant-condition + if (fallbacksQueue.length === 0) return; + + // Convert node to fallback and discard children + suspenseNode = fallbacksQueue.pop(); + fallback = suspenseNode.fallback; + this.convertNodeToFallback(suspenseNode); + + // Stop when found a fallback that requires rendering + if (isRenderableElement(fallback)) break; + } // Render fallback - if (isRenderableElement(fallback)) this.rerender(suspenseNode, fallback); - - // Clear frame to free memory - suspenseNode.frame = null; + this.rerender(suspenseNode, fallback); } rejected(node, err) { @@ -455,12 +478,35 @@ class PartialRenderer extends ReactDOMServerRenderer { // Abort this promise followAndAbort(promise); - // Abort all promises within boundary + // If Suspense containing this node is being processed within this + // render cycle, mark as suspended (to avoid it being added to fallbacks queue) const {suspenseNode} = this; - this.abortDescendents(suspenseNode); + if (suspenseNode.frame) suspenseNode.suspended = true; - // Suspend - suspenseNode.suspended = true; + // Abort all promises within Suspense and + // trigger fallbacks of any nested Suspenses which contain lazy elements + this.suspendDescendents(suspenseNode, false); + } + + suspendDescendents(node, inLazy) { + const {type} = node; + if (type === TYPE_TEXT) return; + + if (type === TYPE_SUSPENSE) { + if (!inLazy && !node.suspended && node.containsLazy) { + node.suspended = true; + this.fallbacksQueue.push(node); + } + } else if (type === TYPE_PROMISE && !node.resolved) { + node.resolved = true; + this.numAwaiting--; + abort(node.promise); + inLazy = true; + } + + for (let child of node.children) { + this.suspendDescendents(child, inLazy); + } } createChildWithStackState(type, frame) { @@ -633,27 +679,6 @@ class PartialRenderer extends ReactDOMServerRenderer { this.clearProviders(); this.contextIndex = -1; } - - /** - * Abort all descendents (children, children's children etc) of a node. - * If promise on lazy component has `.abort()` method, it is called. - * @param {Object} tree - Starting node - * @returns {undefined} - */ - abortDescendents(tree) { - walkTree(tree, node => { - const {type} = node; - if (type === TYPE_TEXT) return false; - - if (type === TYPE_PROMISE && !node.resolved) { - node.resolved = true; - this.numAwaiting--; - abort(node.promise); - } - - return true; - }); - } } module.exports = PartialRenderer;