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
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 committed Nov 11, 2021
1 parent e0aa5e2 commit 4db3dc4
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 4db3dc4

Please sign in to comment.