Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bc3e2f9
Basic partial hydration test
sebmarkbage Jan 18, 2019
433e6e5
Render comments around Suspense components
sebmarkbage Jan 19, 2019
2d28a52
Add DehydratedSuspenseComponent type of work
sebmarkbage Jan 24, 2019
f06a540
Add comment node as hydratable instance type as placeholder for suspense
sebmarkbage Jan 24, 2019
97a6664
Skip past nodes within the Suspense boundary
sebmarkbage Jan 28, 2019
1b3e0a2
A dehydrated suspense boundary comment should be considered a sibling
sebmarkbage Jan 28, 2019
6febcae
Retry hydrating at offscreen pri or after ping if suspended
sebmarkbage Jan 28, 2019
7af0b8a
Enter hydration state when retrying dehydrated suspense boundary
sebmarkbage Jan 28, 2019
dac0688
Delete all children within a dehydrated suspense boundary when it's d…
sebmarkbage Jan 28, 2019
92fb62a
Delete server rendered content when props change before hydration com…
sebmarkbage Jan 29, 2019
6ae914c
Make test internal
sebmarkbage Jan 29, 2019
e144a5e
Wrap in act
sebmarkbage Feb 10, 2019
eb3ea2d
Change SSR Fixture to use Partial Hydration
sebmarkbage Feb 9, 2019
97eb545
Changes to any parent Context forces clearing dehydrated content
sebmarkbage Feb 10, 2019
c91092b
Wrap in feature flag
sebmarkbage Feb 11, 2019
2e16dc1
Treat Suspense boundaries without fallbacks as if not-boundaries
sebmarkbage Feb 12, 2019
1db9dd2
Fix clearing of nested suspense boundaries
sebmarkbage Feb 12, 2019
3a89f65
ping -> retry
acdlite Feb 12, 2019
e0e1ded
Typo
acdlite Feb 12, 2019
ca2d628
Use didReceiveUpdate instead of manually comparing props
sebmarkbage Feb 12, 2019
34a132c
Leave comment for why it's ok to ignore the timeout
sebmarkbage Feb 12, 2019
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
Prev Previous commit
Next Next commit
Wrap in feature flag
  • Loading branch information
sebmarkbage committed Feb 11, 2019
commit c91092b41758df4202645fab58f248803e2bfbd3
9 changes: 7 additions & 2 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
unstable_scheduleCallback as scheduleDeferredCallback,
unstable_cancelCallback as cancelDeferredCallback,
} from 'scheduler';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
export {
unstable_now as now,
unstable_scheduleCallback as scheduleDeferredCallback,
Expand Down Expand Up @@ -562,7 +563,9 @@ export function getNextHydratableSibling(
node &&
node.nodeType !== ELEMENT_NODE &&
node.nodeType !== TEXT_NODE &&
(node.nodeType !== COMMENT_NODE || (node: any).data !== SUSPENSE_START_DATA)
(!enableSuspenseServerRenderer ||
node.nodeType !== COMMENT_NODE ||
(node: any).data !== SUSPENSE_START_DATA)
) {
node = node.nextSibling;
}
Expand All @@ -578,7 +581,9 @@ export function getFirstHydratableChild(
next &&
next.nodeType !== ELEMENT_NODE &&
next.nodeType !== TEXT_NODE &&
(next.nodeType !== COMMENT_NODE || (next: any).data !== SUSPENSE_START_DATA)
(!enableSuspenseServerRenderer ||
next.nodeType !== COMMENT_NODE ||
(next: any).data !== SUSPENSE_START_DATA)
) {
next = next.nextSibling;
}
Expand Down
57 changes: 32 additions & 25 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
debugRenderPhaseSideEffects,
debugRenderPhaseSideEffectsForStrictMode,
enableProfilerTimer,
enableSuspenseServerRenderer,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import shallowEqual from 'shared/shallowEqual';
Expand Down Expand Up @@ -1395,15 +1396,17 @@ function updateSuspenseComponent(
// children -- we skip over the primary children entirely.
let next;
if (current === null) {
// If we're currently hydrating, try to hydrate this boundary.
tryToClaimNextHydratableInstance(workInProgress);
// This could've changed the tag if this was a dehydrated suspense component.
if (workInProgress.tag === DehydratedSuspenseComponent) {
return updateDehydratedSuspenseComponent(
null,
workInProgress,
renderExpirationTime,
);
if (enableSuspenseServerRenderer) {
// If we're currently hydrating, try to hydrate this boundary.
tryToClaimNextHydratableInstance(workInProgress);
// This could've changed the tag if this was a dehydrated suspense component.
if (workInProgress.tag === DehydratedSuspenseComponent) {
return updateDehydratedSuspenseComponent(
null,
workInProgress,
renderExpirationTime,
);
}
}

// This is the initial mount. This branch is pretty simple because there's
Expand Down Expand Up @@ -1972,11 +1975,13 @@ function beginWork(
break;
}
case DehydratedSuspenseComponent: {
// We know that this component will suspend again because if it has
// been unsuspended it has committed as a regular Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.effectTag |= DidCapture;
break;
if (enableSuspenseServerRenderer) {
// We know that this component will suspend again because if it has
// been unsuspended it has committed as a regular Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.effectTag |= DidCapture;
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this break statement doing inside of the if block?

}
}
}
return bailoutOnAlreadyFinishedWork(
Expand Down Expand Up @@ -2148,19 +2153,21 @@ function beginWork(
);
}
case DehydratedSuspenseComponent: {
return updateDehydratedSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
if (enableSuspenseServerRenderer) {
return updateDehydratedSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
}
break;
}
default:
invariant(
false,
'Unknown unit of work tag. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
}
invariant(
false,
'Unknown unit of work tag. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
}

export {beginWork};
6 changes: 5 additions & 1 deletion packages/react-reconciler/src/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
import {
enableSchedulerTracing,
enableProfilerTimer,
enableSuspenseServerRenderer,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
Expand Down Expand Up @@ -1051,7 +1052,10 @@ function unmountHostComponents(current): void {
);
}
// Don't visit children because we already visited them.
} else if (node.tag === DehydratedSuspenseComponent) {
} else if (
enableSuspenseServerRenderer &&
node.tag === DehydratedSuspenseComponent
) {
// Delete the dehydrated suspense boundary and all of its content.
if (currentParentIsContainer) {
clearSuspenseBoundaryFromContainer(
Expand Down
37 changes: 20 additions & 17 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {
skipPastDehydratedSuspenseInstance,
popHydrationState,
} from './ReactFiberHydrationContext';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';

function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
Expand Down Expand Up @@ -765,23 +766,25 @@ function completeWork(
break;
}
case DehydratedSuspenseComponent: {
if (current === null) {
let wasHydrated = popHydrationState(workInProgress);
invariant(
wasHydrated,
'A dehydrated suspense component was completed without a hydrated node. ' +
'This is probably a bug in React.',
);
skipPastDehydratedSuspenseInstance(workInProgress);
} else if ((workInProgress.effectTag & DidCapture) === NoEffect) {
// This boundary did not suspend so it's now hydrated.
// To handle any future suspense cases, we're going to now upgrade it
// to a Suspense component. We detach it from the existing current fiber.
current.alternate = null;
workInProgress.alternate = null;
workInProgress.tag = SuspenseComponent;
workInProgress.memoizedState = null;
workInProgress.stateNode = null;
if (enableSuspenseServerRenderer) {
if (current === null) {
let wasHydrated = popHydrationState(workInProgress);
invariant(
wasHydrated,
'A dehydrated suspense component was completed without a hydrated node. ' +
'This is probably a bug in React.',
);
skipPastDehydratedSuspenseInstance(workInProgress);
} else if ((workInProgress.effectTag & DidCapture) === NoEffect) {
// This boundary did not suspend so it's now hydrated.
// To handle any future suspense cases, we're going to now upgrade it
// to a Suspense component. We detach it from the existing current fiber.
current.alternate = null;
workInProgress.alternate = null;
workInProgress.tag = SuspenseComponent;
workInProgress.memoizedState = null;
workInProgress.stateNode = null;
}
}
break;
}
Expand Down
15 changes: 9 additions & 6 deletions packages/react-reconciler/src/ReactFiberHydrationContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';

// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
Expand Down Expand Up @@ -209,12 +210,14 @@ function tryHydrate(fiber, nextInstance) {
return false;
}
case SuspenseComponent: {
const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
if (suspenseInstance !== null) {
// Downgrade the tag to a dehydrated component until we've hydrated it.
fiber.tag = DehydratedSuspenseComponent;
fiber.stateNode = (suspenseInstance: SuspenseInstance);
return true;
if (enableSuspenseServerRenderer) {
const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
if (suspenseInstance !== null) {
// Downgrade the tag to a dehydrated component until we've hydrated it.
fiber.tag = DehydratedSuspenseComponent;
fiber.stateNode = (suspenseInstance: SuspenseInstance);
return true;
}
}
return false;
}
Expand Down
6 changes: 5 additions & 1 deletion packages/react-reconciler/src/ReactFiberNewContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
} from 'react-reconciler/src/ReactUpdateQueue';
import {NoWork} from './ReactFiberExpirationTime';
import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';

const valueCursor: StackCursor<mixed> = createCursor(null);

Expand Down Expand Up @@ -251,7 +252,10 @@ export function propagateContextChange(
} else if (fiber.tag === ContextProvider) {
// Don't scan deeper if this is a matching provider
nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
} else if (fiber.tag === DehydratedSuspenseComponent) {
} else if (
enableSuspenseServerRenderer &&
fiber.tag === DehydratedSuspenseComponent
) {
// If a dehydrated suspense component is in this subtree, we don't know
// if it will have any context consumers in it. The best we can do is
// mark it as having updates on its children.
Expand Down
31 changes: 18 additions & 13 deletions packages/react-reconciler/src/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
enableUserTimingAPI,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
warnAboutDeprecatedLifecycles,
enableSuspenseServerRenderer,
} from 'shared/ReactFeatureFlags';
import getComponentName from 'shared/getComponentName';
import invariant from 'shared/invariant';
Expand Down Expand Up @@ -1700,19 +1701,23 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, thenable: Thenable) {
// rendering again, at a new expiration time.

let retryCache: WeakSet<Thenable> | Set<Thenable> | null;
switch (boundaryFiber.tag) {
case SuspenseComponent:
retryCache = boundaryFiber.stateNode;
break;
case DehydratedSuspenseComponent:
retryCache = boundaryFiber.memoizedState;
break;
default:
invariant(
false,
'Pinged unknown suspense boundary type. ' +
'This is probably a bug in React.',
);
if (enableSuspenseServerRenderer) {
switch (boundaryFiber.tag) {
case SuspenseComponent:
retryCache = boundaryFiber.stateNode;
break;
case DehydratedSuspenseComponent:
retryCache = boundaryFiber.memoizedState;
break;
default:
invariant(
false,
'Pinged unknown suspense boundary type. ' +
'This is probably a bug in React.',
);
}
} else {
retryCache = boundaryFiber.stateNode;
}
if (retryCache !== null) {
// The thenable resolved, so we no longer need to memoize, because it will
Expand Down
25 changes: 17 additions & 8 deletions packages/react-reconciler/src/ReactFiberUnwindWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ import {
ShouldCapture,
LifecycleEffectMask,
} from 'shared/ReactSideEffectTags';
import {enableSchedulerTracing} from 'shared/ReactFeatureFlags';
import {
enableSchedulerTracing,
enableSuspenseServerRenderer,
} from 'shared/ReactFeatureFlags';
import {ConcurrentMode} from './ReactTypeOfMode';
import {
shouldCaptureSuspense,
Expand Down Expand Up @@ -240,7 +243,10 @@ function throwException(
earliestTimeoutMs = timeoutPropMs;
}
}
} else if (workInProgress.tag === DehydratedSuspenseComponent) {
} else if (
enableSuspenseServerRenderer &&
workInProgress.tag === DehydratedSuspenseComponent
) {
// TODO
}
workInProgress = workInProgress.return;
Expand Down Expand Up @@ -351,6 +357,7 @@ function throwException(
workInProgress.expirationTime = renderExpirationTime;
return;
} else if (
enableSuspenseServerRenderer &&
workInProgress.tag === DehydratedSuspenseComponent &&
shouldCaptureDehydratedSuspense(workInProgress)
) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this branch doesn't suspend (it doesn't call renderDidSuspend) I would expect React to keep rendering the same level over and over until the promise resolves. Is that what's happening?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. What happens is that only the first path schedules remaining work at "Never" expiration time. Then if it throws, it doesn't suspend but it also doesn't leave any work on it. Instead it commits. Then it waits for the retry. The retry gets scheduled at normal priority. If that update also throws a promise, then it commits in the dehydrated state again and waits for the retry.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see so when something suspends inside a dehydrated Suspense boundary it always bails out and clears the expiration time. The ping/retry adds the expiration time back. There’s no need to suspend the commit because it’s not blocking anything.

Expand Down Expand Up @@ -496,12 +503,14 @@ function unwindWork(
return null;
}
case DehydratedSuspenseComponent: {
// TODO: popHydrationState
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
// Captured a suspense effect. Re-render the boundary.
return workInProgress;
if (enableSuspenseServerRenderer) {
// TODO: popHydrationState
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
// Captured a suspense effect. Re-render the boundary.
return workInProgress;
}
}
return null;
}
Expand Down