Skip to content

Commit 34f1c10

Browse files
author
Brian Vaughn
committed
Fixed suspense wrapping heuristic
1 parent 4961833 commit 34f1c10

File tree

2 files changed

+134
-5
lines changed

2 files changed

+134
-5
lines changed

packages/react-devtools-shared/src/__tests__/profilingCache-test.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,4 +716,93 @@ describe('ProfilingCache', () => {
716716
TestRenderer.create(<Validator commitIndex={0} rootID={rootID} />);
717717
});
718718
});
719+
720+
// See https://github.com/facebook/react/issues/18831
721+
it('should not crash during route transitions with Suspense', () => {
722+
const RouterContext = React.createContext();
723+
724+
function App() {
725+
return (
726+
<Router>
727+
<Switch>
728+
<Route path="/">
729+
<Home />
730+
</Route>
731+
<Route path="/about">
732+
<About />
733+
</Route>
734+
</Switch>
735+
</Router>
736+
);
737+
}
738+
739+
const Home = () => {
740+
return (
741+
<React.Suspense>
742+
<Link path="/about">Home</Link>
743+
</React.Suspense>
744+
);
745+
};
746+
747+
const About = () => <div>About</div>;
748+
749+
// Mimics https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Router.js
750+
function Router({children}) {
751+
const [path, setPath] = React.useState('/');
752+
return (
753+
<RouterContext.Provider value={{path, setPath}}>
754+
{children}
755+
</RouterContext.Provider>
756+
);
757+
}
758+
759+
// Mimics https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Switch.js
760+
function Switch({children}) {
761+
return (
762+
<RouterContext.Consumer>
763+
{context => {
764+
let element = null;
765+
React.Children.forEach(children, child => {
766+
if (context.path === child.props.path) {
767+
element = child.props.children;
768+
}
769+
});
770+
return element ? React.cloneElement(element) : null;
771+
}}
772+
</RouterContext.Consumer>
773+
);
774+
}
775+
776+
// Mimics https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Route.js
777+
function Route({children, path}) {
778+
return null;
779+
}
780+
781+
const linkRef = React.createRef();
782+
783+
// Mimics https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js
784+
function Link({children, path}) {
785+
return (
786+
<RouterContext.Consumer>
787+
{context => {
788+
return (
789+
<button ref={linkRef} onClick={() => context.setPath(path)}>
790+
{children}
791+
</button>
792+
);
793+
}}
794+
</RouterContext.Consumer>
795+
);
796+
}
797+
798+
const {Simulate} = require('react-dom/test-utils');
799+
800+
const container = document.createElement('div');
801+
utils.act(() => ReactDOM.render(<App />, container));
802+
expect(container.textContent).toBe('Home');
803+
utils.act(() => store.profilerStore.startProfiling());
804+
utils.act(() => Simulate.click(linkRef.current));
805+
utils.act(() => store.profilerStore.stopProfiling());
806+
expect(container.textContent).toBe('About');
807+
});
719808
});

packages/react-devtools-shared/src/backend/renderer.js

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @flow
88
*/
99

10-
import {gte} from 'semver';
10+
import {gt, gte} from 'semver';
1111
import {
1212
ComponentFilterDisplayName,
1313
ComponentFilterElementType,
@@ -153,7 +153,8 @@ export function getInternalReactConstants(
153153
// **********************************************************
154154
// The section below is copied from files in React repo.
155155
// Keep it in sync, and add version guards if it changes.
156-
if (gte(version, '16.6.0-beta.0')) {
156+
if (gt(version, '16.13.1')) {
157+
// TODO (Offscreen) Update the version number above to reflect the first Offscreen alpha/beta release.
157158
ReactTypeOfWork = {
158159
Block: 22,
159160
ClassComponent: 1,
@@ -181,6 +182,34 @@ export function getInternalReactConstants(
181182
SuspenseListComponent: 19, // Experimental
182183
YieldComponent: -1, // Removed
183184
};
185+
} else if (gte(version, '16.6.0-beta.0')) {
186+
ReactTypeOfWork = {
187+
Block: 22,
188+
ClassComponent: 1,
189+
ContextConsumer: 9,
190+
ContextProvider: 10,
191+
CoroutineComponent: -1, // Removed
192+
CoroutineHandlerPhase: -1, // Removed
193+
DehydratedSuspenseComponent: 18, // Behind a flag
194+
ForwardRef: 11,
195+
Fragment: 7,
196+
FunctionComponent: 0,
197+
HostComponent: 5,
198+
HostPortal: 4,
199+
HostRoot: 3,
200+
HostText: 6,
201+
IncompleteClassComponent: 17,
202+
IndeterminateComponent: 2,
203+
LazyComponent: 16,
204+
MemoComponent: 14,
205+
Mode: 8,
206+
OffscreenComponent: -1, // Experimental
207+
Profiler: 12,
208+
SimpleMemoComponent: 15,
209+
SuspenseComponent: 13,
210+
SuspenseListComponent: 19, // Experimental
211+
YieldComponent: -1, // Removed
212+
};
184213
} else if (gte(version, '16.4.3-alpha')) {
185214
ReactTypeOfWork = {
186215
Block: -1, // Doesn't exist yet
@@ -452,14 +481,16 @@ export function attach(
452481
const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
453482
if (__DEBUG__) {
454483
const displayName = getDisplayNameForFiber(fiber) || 'null';
484+
const id = getFiberID(fiber);
455485
const parentDisplayName =
456486
(parentFiber != null && getDisplayNameForFiber(parentFiber)) || 'null';
487+
const parentID = parentFiber ? getFiberID(parentFiber) : '';
457488
// NOTE: calling getFiberID or getPrimaryFiber is unsafe here
458489
// because it will put them in the map. For now, we'll omit them.
459490
// TODO: better debugging story for this.
460491
console.log(
461-
`[renderer] %c${name} %c${displayName} %c${
462-
parentFiber ? parentDisplayName : ''
492+
`[renderer] %c${name} %c${displayName} (${id}) %c${
493+
parentFiber ? `${parentDisplayName} (${parentID})` : ''
463494
}`,
464495
'color: red; font-weight: bold;',
465496
'color: blue;',
@@ -1076,6 +1107,10 @@ export function attach(
10761107
}
10771108

10781109
function recordMount(fiber: Fiber, parentFiber: Fiber | null) {
1110+
if (__DEBUG__) {
1111+
debug('recordMount()', fiber, parentFiber);
1112+
}
1113+
10791114
const isRoot = fiber.tag === HostRoot;
10801115
const id = getFiberID(getPrimaryFiber(fiber));
10811116

@@ -1130,6 +1165,10 @@ export function attach(
11301165
}
11311166

11321167
function recordUnmount(fiber: Fiber, isSimulated: boolean) {
1168+
if (__DEBUG__) {
1169+
debug('recordUnmount()', fiber);
1170+
}
1171+
11331172
if (trackedPathMatchFiber !== null) {
11341173
// We're in the process of trying to restore previous selection.
11351174
// If this fiber matched but is being unmounted, there's no use trying.
@@ -1215,7 +1254,8 @@ export function attach(
12151254
// because we don't want to highlight every host node inside of a newly mounted subtree.
12161255
}
12171256

1218-
if (fiber.tag === ReactTypeOfWork.SuspenseComponent) {
1257+
const isSuspense = fiber.tag === ReactTypeOfWork.SuspenseComponent;
1258+
if (isSuspense) {
12191259
const isTimedOut = fiber.memoizedState !== null;
12201260
if (isTimedOut) {
12211261
// Special case: if Suspense mounts in a timed-out state,

0 commit comments

Comments
 (0)