Skip to content

Commit 4db3dc4

Browse files
author
Brian Vaughn
committed
DevTools should properly report re-renders due to (use)context changes
Note that this only fixes things for newer versions of React (e.g. 18 alpha). Older versions will remain broken because there's not a good way to read the most recent context value for a location in the tree after render has completed. This is because React maintains a stack of context values during render, but by the time DevTools is called– render has finished and the stack is empty.
1 parent e0aa5e2 commit 4db3dc4

File tree

2 files changed

+162
-1
lines changed

2 files changed

+162
-1
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
describe('Profiler change descriptions', () => {
11+
let React;
12+
let legacyRender;
13+
let store: Store;
14+
let utils;
15+
16+
beforeEach(() => {
17+
utils = require('./utils');
18+
utils.beforeEachProfiling();
19+
20+
legacyRender = utils.legacyRender;
21+
22+
store = global.store;
23+
store.collapseNodesByDefault = false;
24+
store.recordChangeDescriptions = true;
25+
26+
React = require('react');
27+
});
28+
29+
it('should identify useContext as the cause for a re-render', () => {
30+
const Context = React.createContext(0);
31+
32+
function Child() {
33+
const context = React.useContext(Context);
34+
return context;
35+
}
36+
37+
function areEqual() {
38+
return true;
39+
}
40+
41+
const MemoizedChild = React.memo(Child, areEqual);
42+
const ForwardRefChild = React.forwardRef(function RefForwardingComponent(
43+
props,
44+
ref,
45+
) {
46+
return <Child />;
47+
});
48+
49+
let forceUpdate = null;
50+
51+
const App = function App() {
52+
const [val, dispatch] = React.useReducer(x => x + 1, 0);
53+
54+
forceUpdate = dispatch;
55+
56+
return (
57+
<Context.Provider value={val}>
58+
<Child />
59+
<MemoizedChild />
60+
<ForwardRefChild />
61+
</Context.Provider>
62+
);
63+
};
64+
65+
const container = document.createElement('div');
66+
67+
utils.act(() => store.profilerStore.startProfiling());
68+
utils.act(() => legacyRender(<App />, container));
69+
utils.act(() => forceUpdate());
70+
utils.act(() => store.profilerStore.stopProfiling());
71+
72+
const rootID = store.roots[0];
73+
const commitData = store.profilerStore.getCommitData(rootID, 1);
74+
75+
expect(store).toMatchInlineSnapshot(`
76+
[root]
77+
▾ <App>
78+
▾ <Context.Provider>
79+
<Child>
80+
▾ <Child> [Memo]
81+
<Child>
82+
▾ <RefForwardingComponent> [ForwardRef]
83+
<Child>
84+
`);
85+
86+
let element = store.getElementAtIndex(2);
87+
expect(element.displayName).toBe('Child');
88+
expect(element.hocDisplayNames).toBeNull();
89+
expect(commitData.changeDescriptions.get(element.id))
90+
.toMatchInlineSnapshot(`
91+
Object {
92+
"context": true,
93+
"didHooksChange": false,
94+
"hooks": null,
95+
"isFirstMount": false,
96+
"props": Array [],
97+
"state": null,
98+
}
99+
`);
100+
101+
element = store.getElementAtIndex(3);
102+
expect(element.displayName).toBe('Child');
103+
expect(element.hocDisplayNames).toEqual(['Memo']);
104+
expect(commitData.changeDescriptions.get(element.id)).toBeUndefined();
105+
106+
element = store.getElementAtIndex(4);
107+
expect(element.displayName).toBe('Child');
108+
expect(element.hocDisplayNames).toBeNull();
109+
expect(commitData.changeDescriptions.get(element.id))
110+
.toMatchInlineSnapshot(`
111+
Object {
112+
"context": true,
113+
"didHooksChange": false,
114+
"hooks": null,
115+
"isFirstMount": false,
116+
"props": Array [],
117+
"state": null,
118+
}
119+
`);
120+
121+
element = store.getElementAtIndex(5);
122+
expect(element.displayName).toBe('RefForwardingComponent');
123+
expect(element.hocDisplayNames).toEqual(['ForwardRef']);
124+
expect(commitData.changeDescriptions.get(element.id))
125+
.toMatchInlineSnapshot(`
126+
Object {
127+
"context": null,
128+
"didHooksChange": false,
129+
"hooks": null,
130+
"isFirstMount": false,
131+
"props": Array [],
132+
"state": null,
133+
}
134+
`);
135+
136+
element = store.getElementAtIndex(6);
137+
expect(element.displayName).toBe('Child');
138+
expect(element.hocDisplayNames).toBeNull();
139+
expect(commitData.changeDescriptions.get(element.id))
140+
.toMatchInlineSnapshot(`
141+
Object {
142+
"context": true,
143+
"didHooksChange": false,
144+
"hooks": null,
145+
"isFirstMount": false,
146+
"props": Array [],
147+
"state": null,
148+
}
149+
`);
150+
});
151+
});

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1253,8 +1253,10 @@ export function attach(
12531253

12541254
function updateContextsForFiber(fiber: Fiber) {
12551255
switch (getElementTypeForFiber(fiber)) {
1256-
case ElementTypeFunction:
12571256
case ElementTypeClass:
1257+
case ElementTypeForwardRef:
1258+
case ElementTypeFunction:
1259+
case ElementTypeMemo:
12581260
if (idToContextsMap !== null) {
12591261
const id = getFiberIDThrows(fiber);
12601262
const contexts = getContextsForFiber(fiber);
@@ -1292,7 +1294,9 @@ export function attach(
12921294
}
12931295
}
12941296
return [legacyContext, modernContext];
1297+
case ElementTypeForwardRef:
12951298
case ElementTypeFunction:
1299+
case ElementTypeMemo:
12961300
const dependencies = fiber.dependencies;
12971301
if (dependencies && dependencies.firstContext) {
12981302
modernContext = dependencies.firstContext;
@@ -1341,12 +1345,18 @@ export function attach(
13411345
}
13421346
}
13431347
break;
1348+
case ElementTypeForwardRef:
13441349
case ElementTypeFunction:
1350+
case ElementTypeMemo:
13451351
if (nextModernContext !== NO_CONTEXT) {
13461352
let prevContext = prevModernContext;
13471353
let nextContext = nextModernContext;
13481354

13491355
while (prevContext && nextContext) {
1356+
// Note this only works for versions of React that support this key (e.v. 18+)
1357+
// For older versions, there's no good way to read the current context value after render has completed.
1358+
// This is because React maintains a stack of context values during render,
1359+
// but by the time DevTools is called, render has finished and the stack is empty.
13501360
if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) {
13511361
return true;
13521362
}

0 commit comments

Comments
 (0)