Skip to content

Commit

Permalink
WIP Render fallbacks when suspended above
Browse files Browse the repository at this point in the history
  • Loading branch information
overlookmotel committed Mar 17, 2019
1 parent b6a8f96 commit 218a763
Showing 1 changed file with 67 additions and 42 deletions.
109 changes: 67 additions & 42 deletions lib/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class PartialRenderer extends ReactDOMServerRenderer {

// Init lazy node awaiting counter
this.numAwaiting = 0;
this.fallbacksQueue = [];

// Init interrupt signal
this.interrupt = null;
Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;

0 comments on commit 218a763

Please sign in to comment.