Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow DevTools to toggle Suspense fallbacks #15232

Merged
merged 6 commits into from Apr 4, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -14,13 +14,18 @@ describe('React hooks DevTools integration', () => {
let React;
let ReactDebugTools;
let ReactTestRenderer;
let Scheduler;
let act;
let overrideHookState;
let overrideProps;
let overrideSuspense;

beforeEach(() => {
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
inject: injected => {
overrideHookState = injected.overrideHookState;
overrideProps = injected.overrideProps;
overrideSuspense = injected.overrideSuspense;
},
supportsFiber: true,
onCommitFiberRoot: () => {},
Expand All @@ -32,6 +37,7 @@ describe('React hooks DevTools integration', () => {
React = require('react');
ReactDebugTools = require('react-debug-tools');
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');

act = ReactTestRenderer.act;
});
Expand Down Expand Up @@ -173,4 +179,112 @@ describe('React hooks DevTools integration', () => {
});
}
});

it('should support overriding suspense in sync mode', () => {
if (__DEV__) {
// Lock the first render
overrideSuspense(() => true);
}

function MyComponent() {
return 'Done';
}

const renderer = ReactTestRenderer.create(
<div>
<React.Suspense fallback={'Loading'}>
<MyComponent />
</React.Suspense>
</div>,
);
const fiber = renderer.root._currentFiber().child;
if (__DEV__) {
// First render was locked
expect(renderer.toJSON().children).toEqual(['Loading']);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);

// Release the lock
overrideSuspense(() => false);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);

// Lock again
overrideSuspense(() => true);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);

// Release the lock again
overrideSuspense(() => false);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);

// Ensure it checks specific fibers.
overrideSuspense(f => f === fiber || f === fiber.alternate);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);
overrideSuspense(f => f !== fiber && f !== fiber.alternate);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
} else {
expect(renderer.toJSON().children).toEqual(['Done']);
}
});

it('should support overriding suspense in concurrent mode', () => {
if (__DEV__) {
// Lock the first render
overrideSuspense(() => true);
}

function MyComponent() {
return 'Done';
}

const renderer = ReactTestRenderer.create(
<div>
<React.Suspense fallback={'Loading'}>
<MyComponent />
</React.Suspense>
</div>,
{unstable_isConcurrent: true},
);
expect(Scheduler).toFlushAndYield([]);
const fiber = renderer.root._currentFiber().child;
if (__DEV__) {
// First render was locked
expect(renderer.toJSON().children).toEqual(['Loading']);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);

// Release the lock
overrideSuspense(() => false);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);

// Lock again
overrideSuspense(() => true);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);

// Release the lock again
overrideSuspense(() => false);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);

// Ensure it checks specific fibers.
overrideSuspense(f => f === fiber || f === fiber.alternate);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);
overrideSuspense(f => f !== fiber && f !== fiber.alternate);
overrideProps(fiber, [], null); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
} else {
expect(renderer.toJSON().children).toEqual(['Done']);
}
});
});
7 changes: 7 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Expand Up @@ -96,6 +96,7 @@ import {
registerSuspenseInstanceRetry,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldSuspend} from './ReactFiberReconciler';
import {
pushHostContext,
pushHostContainer,
Expand Down Expand Up @@ -1392,6 +1393,12 @@ function updateSuspenseComponent(
const mode = workInProgress.mode;
const nextProps = workInProgress.pendingProps;

if (__DEV__) {
if (shouldSuspend(workInProgress)) {
workInProgress.effectTag |= DidCapture;
Copy link
Collaborator

@acdlite acdlite Mar 29, 2019

Choose a reason for hiding this comment

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

Suggestion: replace this with

throwException(
  root,
  workInProgress, // returnFiber
  null, // sourceFiber
  dummyPromiseThatNeverResolves,
  renderExpirationTime
);

This will require refactoring throwException a bit, or perhaps splitting it into multiple functions, since it currently assumes that you have a source fiber.

EDIT: Never mind, I forgot you also need to call unwindWork in concurrent mode... but not in sync mode.

What motivated this suggestion is that there are a lot of subtle differences to account for between the two modes and I'm paranoid about neglecting one of them.

}
}

// We should attempt to render the primary children unless this boundary
// already suspended during this render (`alreadyCaptured` is true).
let nextState: SuspenseState | null = workInProgress.memoizedState;
Expand Down
18 changes: 17 additions & 1 deletion packages/react-reconciler/src/ReactFiberReconciler.js
Expand Up @@ -340,8 +340,15 @@ export function findHostInstanceWithNoPortals(
return hostFiber.stateNode;
}

let shouldSuspendImpl = fiber => false;

export function shouldSuspend(fiber: Fiber): boolean {
return shouldSuspendImpl(fiber);
}

let overrideHookState = null;
let overrideProps = null;
let overrideSuspense = null;

if (__DEV__) {
const copyWithSetImpl = (
Expand Down Expand Up @@ -403,12 +410,20 @@ if (__DEV__) {
// Support DevTools props for function components, forwardRef, memo, host components, etc.
overrideProps = (fiber: Fiber, path: Array<string | number>, value: any) => {
flushPassiveEffects();
fiber.pendingProps = copyWithSet(fiber.memoizedProps, path, value);
if (path.length > 0) {
fiber.pendingProps = copyWithSet(fiber.memoizedProps, path, value);
} else {
fiber.pendingProps = {...fiber.pendingProps};
}
if (fiber.alternate) {
fiber.alternate.pendingProps = fiber.pendingProps;
}
scheduleWork(fiber, Sync);
};

overrideSuspense = (newShouldSuspendImpl: Fiber => boolean) => {
shouldSuspendImpl = newShouldSuspendImpl;
};
}

export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
Expand All @@ -419,6 +434,7 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
...devToolsConfig,
overrideHookState,
overrideProps,
overrideSuspense,
currentDispatcherRef: ReactCurrentDispatcher,
findHostInstanceByFiber(fiber: Fiber): Instance | TextInstance | null {
const hostFiber = findCurrentHostFiber(fiber);
Expand Down