diff --git a/packages/react-devtools-shared/src/__tests__/profilerChangeDescriptions-test.js b/packages/react-devtools-shared/src/__tests__/profilerChangeDescriptions-test.js
new file mode 100644
index 0000000000000..eee3a66b03a55
--- /dev/null
+++ b/packages/react-devtools-shared/src/__tests__/profilerChangeDescriptions-test.js
@@ -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 ;
+ });
+
+ let forceUpdate = null;
+
+ const App = function App() {
+ const [val, dispatch] = React.useReducer(x => x + 1, 0);
+
+ forceUpdate = dispatch;
+
+ return (
+
+
+
+
+
+ );
+ };
+
+ const container = document.createElement('div');
+
+ utils.act(() => store.profilerStore.startProfiling());
+ utils.act(() => legacyRender(, 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]
+ ▾
+ ▾
+
+ ▾ [Memo]
+
+ ▾ [ForwardRef]
+
+ `);
+
+ 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,
+ }
+ `);
+ });
+});
diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js
index 9d2005522da4b..55de04d02b71a 100644
--- a/packages/react-devtools-shared/src/backend/renderer.js
+++ b/packages/react-devtools-shared/src/backend/renderer.js
@@ -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);
@@ -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;
@@ -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;
}