@samitouri / QOS-React-2 / commits / 56efc65e58

DevTools should properly report re-renders due to (use)context changes (#22746)

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.

Brian Vaughn committed Nov 11, 2021 at 14:22 UTC 56efc65e5857abe22062c9506207abc35c9532d1
2 files changed +162 -1
packages/react-devtools-shared/src/__tests__/profilerChangeDescriptions-test.js new
+151
@@ -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
+11 -1
@@ -1253,8 +1253,10 @@ export function attach(
1253
1254 function updateContextsForFiber(fiber: Fiber) {
1255 switch (getElementTypeForFiber(fiber)) {
1256 - case ElementTypeFunction:
1256 case ElementTypeClass:
1257 + case ElementTypeForwardRef:
1258 + case ElementTypeFunction:
1259 + case ElementTypeMemo:
1260 if (idToContextsMap !== null) {
1261 const id = getFiberIDThrows(fiber);
1262 const contexts = getContextsForFiber(fiber);
@@ -1292,7 +1294,9 @@ export function attach(
1294 }
1295 }
1296 return [legacyContext, modernContext];
1297 + case ElementTypeForwardRef:
1298 case ElementTypeFunction:
1299 + case ElementTypeMemo:
1300 const dependencies = fiber.dependencies;
1301 if (dependencies && dependencies.firstContext) {
1302 modernContext = dependencies.firstContext;
@@ -1341,12 +1345,18 @@ export function attach(
1345 }
1346 }
1347 break;
1348 + case ElementTypeForwardRef:
1349 case ElementTypeFunction:
1350 + case ElementTypeMemo:
1351 if (nextModernContext !== NO_CONTEXT) {
1352 let prevContext = prevModernContext;
1353 let nextContext = nextModernContext;
1354
1355 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.
1360 if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) {
1361 return true;
1362 }