Skip to content

Commit

Permalink
gate console errors to __DEV__
Browse files Browse the repository at this point in the history
  • Loading branch information
salazarm committed Jan 27, 2022
1 parent 505c15c commit 2e2efb6
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.innerHTML).toContain('<div>Sibling</div>');
});

it('recovers when server rendered additional nodes', async () => {
it('recovers with client render when server rendered additional nodes at suspense root', async () => {
const ref = React.createRef();
function App({hasB}) {
return (
Expand All @@ -462,15 +462,143 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.innerHTML).toContain('<span>B</span>');
expect(ref.current).toBe(null);

ReactDOM.hydrateRoot(container, <App hasB={false} />);
expect(() => {
Scheduler.unstable_flushAll();
act(() => {
ReactDOM.hydrateRoot(container, <App hasB={false} />);
});
}).toErrorDev('Did not expect server HTML to contain a <span> in <div>');

jest.runAllTimers();

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
expect(ref.current).toBe(span);

if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
expect(ref.current).not.toBe(span);
} else {
expect(ref.current).toBe(span);
}
});

it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {
spyOnDev(console, 'error');
const ref = React.createRef();
function App({hasB}) {
return (
<div>
<Suspense fallback="Loading...">
<Suspender />
<span ref={ref}>A</span>
{hasB ? <span>B</span> : null}
</Suspense>
<div>Sibling</div>
</div>
);
}

let shouldSuspend = false;
let resolve;
const promise = new Promise(res => {
resolve = () => {
shouldSuspend = false;
res();
};
});
function Suspender() {
if (shouldSuspend) {
throw promise;
}
return <div />;
}

const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);

const container = document.createElement('div');
container.innerHTML = finalHTML;

const span = container.getElementsByTagName('span')[0];

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).toContain('<span>B</span>');
expect(ref.current).toBe(null);

shouldSuspend = true;
act(() => {
ReactDOM.hydrateRoot(container, <App hasB={false} />);
});

resolve();
await promise;
Scheduler.unstable_flushAll();
await null;
jest.runAllTimers();

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
expect(ref.current).not.toBe(span);
} else {
expect(ref.current).toBe(span);
}

if (__DEV__) {
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
expect(console.error.calls.count()).toBe(2);
const errorArgs = console.error.calls.all().shift().args;
expect(errorArgs[0]).toContain(
'An error occurred during hydration. The server HTML was replaced with client content',
);
} else {
expect(console.error.calls.count()).toBe(1);
}

const errorArgs = console.error.calls.all().pop().args;
expect(errorArgs[0]).toBe(
'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
);
}
});

it('recovers with client render when server rendered additional nodes deep inside suspense root', async () => {
const ref = React.createRef();
function App({hasB}) {
return (
<div>
<Suspense fallback="Loading...">
<div>
<span ref={ref}>A</span>
{hasB ? <span>B</span> : null}
</div>
</Suspense>
<div>Sibling</div>
</div>
);
}

const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);

const container = document.createElement('div');
container.innerHTML = finalHTML;

const span = container.getElementsByTagName('span')[0];

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).toContain('<span>B</span>');
expect(ref.current).toBe(null);

expect(() => {
act(() => {
ReactDOM.hydrateRoot(container, <App hasB={false} />);
});
}).toErrorDev('Did not expect server HTML to contain a <span> in <div>');

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
expect(ref.current).not.toBe(span);
} else {
expect(ref.current).toBe(span);
}
});

it('calls the onDeleted hydration callback if the parent gets deleted', async () => {
Expand Down
78 changes: 57 additions & 21 deletions packages/react-reconciler/src/ReactFiberCompleteWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import type {
import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.new';
import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
import {
enableClientRenderFallbackOnHydrationMismatch,
enableSuspenseAvoidThisFallback,
} from 'shared/ReactFeatureFlags';

import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';

Expand Down Expand Up @@ -74,6 +77,9 @@ import {
StaticMask,
MutationMask,
Passive,
Incomplete,
ShouldCapture,
ForceClientRender,
} from './ReactFiberFlags';

import {
Expand Down Expand Up @@ -120,9 +126,11 @@ import {
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
prepareToHydrateHostSuspenseInstance,
warnDeleteNextHydratableInstance,
popHydrationState,
resetHydrationState,
getIsHydrating,
hasMore,
} from './ReactFiberHydrationContext.new';
import {
enableSuspenseCallback,
Expand Down Expand Up @@ -828,7 +836,6 @@ function completeWork(
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
Expand All @@ -840,9 +847,11 @@ function completeWork(
case Profiler:
case ContextConsumer:
case MemoComponent:
popTreeContext(workInProgress);
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
popTreeContext(workInProgress);
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
Expand All @@ -852,6 +861,23 @@ function completeWork(
}
case HostRoot: {
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (current === null || current.child === null) {
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.
markUpdate(workInProgress);
} else if (!fiberRoot.isDehydrated) {
// Schedule an effect to clear this container at the start of the next commit.
// This handles the case of React rendering into a container with previous children.
// It's also safe to do for updates too, because current.child would only be null
// if the previous render was null (so the container would already be empty).
workInProgress.flags |= Snapshot;
}
}
popTreeContext(workInProgress);
if (enableCache) {
popRootCachePool(fiberRoot, renderLanes);

Expand All @@ -873,27 +899,13 @@ function completeWork(
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
if (current === null || current.child === null) {
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.
markUpdate(workInProgress);
} else if (!fiberRoot.isDehydrated) {
// Schedule an effect to clear this container at the start of the next commit.
// This handles the case of React rendering into a container with previous children.
// It's also safe to do for updates too, because current.child would only be null
// if the previous render was null (so the container would already be empty).
workInProgress.flags |= Snapshot;
}
}
updateHostContainer(current, workInProgress);
bubbleProperties(workInProgress);
return null;
}
case HostComponent: {
const wasHydrated = popHydrationState(workInProgress);
popTreeContext(workInProgress);
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
Expand Down Expand Up @@ -928,7 +940,6 @@ function completeWork(
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on whether we want to add them top->down or
// bottom->up. Top->down is faster in IE11.
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
Expand Down Expand Up @@ -981,6 +992,7 @@ function completeWork(
return null;
}
case HostText: {
popTreeContext(workInProgress);
const newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
Expand Down Expand Up @@ -1017,14 +1029,27 @@ function completeWork(
return null;
}
case SuspenseComponent: {
popSuspenseContext(workInProgress);
const nextState: null | SuspenseState = workInProgress.memoizedState;

if (enableSuspenseServerRenderer) {
if (
enableClientRenderFallbackOnHydrationMismatch &&
hasMore() &&
(workInProgress.flags & DidCapture) === NoFlags
) {
warnDeleteNextHydratableInstance(workInProgress);
resetHydrationState();
workInProgress.flags |=
ForceClientRender | Incomplete | ShouldCapture;
popTreeContext(workInProgress);
popSuspenseContext(workInProgress);
return workInProgress;
}
if (nextState !== null && nextState.dehydrated !== null) {
// We might be inside a hydration state the first time we're picking up this
// Suspense boundary, and also after we've reentered it for further hydration.
const wasHydrated = popHydrationState(workInProgress);
popTreeContext(workInProgress);
popSuspenseContext(workInProgress);
if (current === null) {
if (!wasHydrated) {
throw new Error(
Expand Down Expand Up @@ -1091,6 +1116,8 @@ function completeWork(
) {
transferActualDuration(workInProgress);
}
popTreeContext(workInProgress);
popSuspenseContext(workInProgress);
// Don't bubble properties in this case.
return workInProgress;
}
Expand All @@ -1103,6 +1130,8 @@ function completeWork(
const prevState: null | SuspenseState = current.memoizedState;
prevDidTimeout = prevState !== null;
}
popTreeContext(workInProgress);
popSuspenseContext(workInProgress);

if (enableCache && nextDidTimeout) {
const offscreenFiber: Fiber = (workInProgress.child: any);
Expand Down Expand Up @@ -1207,6 +1236,7 @@ function completeWork(
return null;
}
case HostPortal:
popTreeContext(workInProgress);
popHostContainer(workInProgress);
updateHostContainer(current, workInProgress);
if (current === null) {
Expand All @@ -1215,12 +1245,14 @@ function completeWork(
bubbleProperties(workInProgress);
return null;
case ContextProvider:
popTreeContext(workInProgress);
// Pop provider fiber
const context: ReactContext<any> = workInProgress.type._context;
popProvider(context, workInProgress);
bubbleProperties(workInProgress);
return null;
case IncompleteClassComponent: {
popTreeContext(workInProgress);
// Same as class component case. I put it down here so that the tags are
// sequential to ensure this switch is compiled to a jump table.
const Component = workInProgress.type;
Expand All @@ -1231,6 +1263,7 @@ function completeWork(
return null;
}
case SuspenseListComponent: {
popTreeContext(workInProgress);
popSuspenseContext(workInProgress);

const renderState: null | SuspenseListRenderState =
Expand Down Expand Up @@ -1440,6 +1473,7 @@ function completeWork(
return null;
}
case ScopeComponent: {
popTreeContext(workInProgress);
if (enableScopeAPI) {
if (current === null) {
const scopeInstance: ReactScopeInstance = createScopeInstance();
Expand All @@ -1464,6 +1498,7 @@ function completeWork(
}
case OffscreenComponent:
case LegacyHiddenComponent: {
popTreeContext(workInProgress);
popRenderLanes(workInProgress);
const nextState: OffscreenState | null = workInProgress.memoizedState;
const nextIsHidden = nextState !== null;
Expand Down Expand Up @@ -1532,6 +1567,7 @@ function completeWork(
return null;
}
case CacheComponent: {
popTreeContext(workInProgress);
if (enableCache) {
let previousCache: Cache | null = null;
if (workInProgress.alternate !== null) {
Expand Down
Loading

0 comments on commit 2e2efb6

Please sign in to comment.