Skip to content

Commit

Permalink
DevTools should properly report re-renders due to (use)context changes (
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
Brian Vaughn authored Nov 11, 2021
1 parent a44a7a2 commit 56efc65
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

describe('Profiler change descriptions', () => {
let React;
let legacyRender;
let store: Store;
let utils;

beforeEach(() => {
utils = require('./utils');
utils.beforeEachProfiling();

legacyRender = utils.legacyRender;

store = global.store;
store.collapseNodesByDefault = false;
store.recordChangeDescriptions = true;

React = require('react');
});

it('should identify useContext as the cause for a re-render', () => {
const Context = React.createContext(0);

function Child() {
const context = React.useContext(Context);
return context;
}

function areEqual() {
return true;
}

const MemoizedChild = React.memo(Child, areEqual);
const ForwardRefChild = React.forwardRef(function RefForwardingComponent(
props,
ref,
) {
return <Child />;
});

let forceUpdate = null;

const App = function App() {
const [val, dispatch] = React.useReducer(x => x + 1, 0);

forceUpdate = dispatch;

return (
<Context.Provider value={val}>
<Child />
<MemoizedChild />
<ForwardRefChild />
</Context.Provider>
);
};

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

utils.act(() => store.profilerStore.startProfiling());
utils.act(() => legacyRender(<App />, container));
utils.act(() => forceUpdate());
utils.act(() => store.profilerStore.stopProfiling());

const rootID = store.roots[0];
const commitData = store.profilerStore.getCommitData(rootID, 1);

expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Context.Provider>
<Child>
▾ <Child> [Memo]
<Child>
▾ <RefForwardingComponent> [ForwardRef]
<Child>
`);

let element = store.getElementAtIndex(2);
expect(element.displayName).toBe('Child');
expect(element.hocDisplayNames).toBeNull();
expect(commitData.changeDescriptions.get(element.id))
.toMatchInlineSnapshot(`
Object {
"context": true,
"didHooksChange": false,
"hooks": null,
"isFirstMount": false,
"props": Array [],
"state": null,
}
`);

element = store.getElementAtIndex(3);
expect(element.displayName).toBe('Child');
expect(element.hocDisplayNames).toEqual(['Memo']);
expect(commitData.changeDescriptions.get(element.id)).toBeUndefined();

element = store.getElementAtIndex(4);
expect(element.displayName).toBe('Child');
expect(element.hocDisplayNames).toBeNull();
expect(commitData.changeDescriptions.get(element.id))
.toMatchInlineSnapshot(`
Object {
"context": true,
"didHooksChange": false,
"hooks": null,
"isFirstMount": false,
"props": Array [],
"state": null,
}
`);

element = store.getElementAtIndex(5);
expect(element.displayName).toBe('RefForwardingComponent');
expect(element.hocDisplayNames).toEqual(['ForwardRef']);
expect(commitData.changeDescriptions.get(element.id))
.toMatchInlineSnapshot(`
Object {
"context": null,
"didHooksChange": false,
"hooks": null,
"isFirstMount": false,
"props": Array [],
"state": null,
}
`);

element = store.getElementAtIndex(6);
expect(element.displayName).toBe('Child');
expect(element.hocDisplayNames).toBeNull();
expect(commitData.changeDescriptions.get(element.id))
.toMatchInlineSnapshot(`
Object {
"context": true,
"didHooksChange": false,
"hooks": null,
"isFirstMount": false,
"props": Array [],
"state": null,
}
`);
});
});
12 changes: 11 additions & 1 deletion packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1253,8 +1253,10 @@ export function attach(

function updateContextsForFiber(fiber: Fiber) {
switch (getElementTypeForFiber(fiber)) {
case ElementTypeFunction:
case ElementTypeClass:
case ElementTypeForwardRef:
case ElementTypeFunction:
case ElementTypeMemo:
if (idToContextsMap !== null) {
const id = getFiberIDThrows(fiber);
const contexts = getContextsForFiber(fiber);
Expand Down Expand Up @@ -1292,7 +1294,9 @@ export function attach(
}
}
return [legacyContext, modernContext];
case ElementTypeForwardRef:
case ElementTypeFunction:
case ElementTypeMemo:
const dependencies = fiber.dependencies;
if (dependencies && dependencies.firstContext) {
modernContext = dependencies.firstContext;
Expand Down Expand Up @@ -1341,12 +1345,18 @@ export function attach(
}
}
break;
case ElementTypeForwardRef:
case ElementTypeFunction:
case ElementTypeMemo:
if (nextModernContext !== NO_CONTEXT) {
let prevContext = prevModernContext;
let nextContext = nextModernContext;

while (prevContext && nextContext) {
// Note this only works for versions of React that support this key (e.v. 18+)
// For older versions, there's no good way to read the current context value 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.
if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) {
return true;
}
Expand Down

0 comments on commit 56efc65

Please sign in to comment.