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 531ba7e
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 51 deletions.
108 changes: 65 additions & 43 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,17 @@ class PartialRenderer extends ReactDOMServerRenderer {

errored(err) {
// Abort all promises
this.abortDescendents(this.tree);
walkTree(this.tree, node => {
const {type} = node;
if (type === TYPE_SUSPENSE) {
if (node.suspended) return walkTree.STOP;
} else if (type === TYPE_PROMISE && !node.resolved) {
this.abort(node);
}
});

// Clear fallbacks queue
this.fallbacksQueue.length = 0;

// Record error
this.error = err;
Expand Down Expand Up @@ -276,6 +284,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 +311,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,10 +396,12 @@ 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);

// Clear stack state on node
node.stackState = null;

// Render element
this.cycle();

Expand All @@ -397,18 +411,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 All @@ -418,6 +439,7 @@ class PartialRenderer extends ReactDOMServerRenderer {

// Promise is no longer awaited
node.resolved = true;
node.stackState = null;
this.numAwaiting--;

// Abort all rendering
Expand Down Expand Up @@ -455,12 +477,33 @@ class PartialRenderer extends ReactDOMServerRenderer {
// Abort this promise
followAndAbort(promise);

// Abort all promises within boundary
const {suspenseNode} = this;
this.abortDescendents(suspenseNode);
// Abort all promises within Suspense and trigger fallbacks
// of any nested Suspenses which contain lazy elements.
// NB Do not add first Suspense to fallbacks queue if rendering in this cycle.
const {suspenseNode, fallbacksQueue} = this;
walkTree(suspenseNode, (node, inLazy) => {
const {type} = node;
if (type === TYPE_SUSPENSE) {
if (node.suspended) return walkTree.STOP;

// Suspend
suspenseNode.suspended = true;
if (!inLazy && node.containsLazy) {
node.suspended = true;
if (!node.frame) fallbacksQueue.push(node);
}
} else if (type === TYPE_PROMISE) {
if (!node.resolved) this.abort(node);
inLazy = true;
}

return inLazy;
}, false);
}

abort(node) {
node.resolved = true;
node.stackState = null;
abort(node.promise);
this.numAwaiting--;
}

createChildWithStackState(type, frame) {
Expand Down Expand Up @@ -633,27 +676,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;
3 changes: 1 addition & 2 deletions lib/treeToHtml.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ function treeToHtml(tree, makeStaticMarkup) {
lastText = false;

walkTree(tree, node => {
if (node.type !== TYPE_TEXT) return true;
if (node.type !== TYPE_TEXT) return;

const thisOut = node.out;
if (lastText && thisOut.slice(0, 1) !== '<') out += '<!-- -->';
out += thisOut;
if (!makeStaticMarkup) lastText = thisOut.slice(-1) !== '>';
return false;
});

return out;
Expand Down
22 changes: 16 additions & 6 deletions lib/walkTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,32 @@

'use strict';

// Constants
const STOP = {};

// Exports

/**
* Walk tree and call `fn()` on each node.
* Stop walking branch if `fn()` returns `false`.
* `fn()` can return state which is passed to its child nodes.
* Stop walking branch if `fn()` returns `walkTree.STOP`.
* @param {Object} node - Boundary tree node
* @param {Function} fn - Function to call on each node
* @param {*} [state] - Optional state to
* @returns {undefined}
*/
function walkTree(node, fn) {
const continueWalking = fn(node);
if (!continueWalking) return;
function walkTree(node, fn, state) {
state = fn(node, state);
if (state === STOP) return;

const {children} = node;
if (!children) return;

for (let child of node.children) {
walkTree(child, fn);
for (let child of children) {
walkTree(child, fn, state);
}
}

walkTree.STOP = STOP;

module.exports = walkTree;

0 comments on commit 531ba7e

Please sign in to comment.