Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -568,9 +568,10 @@ function processDebugValues(
}

export function inspectHooks<Props>(
renderFunction: Props => React$Node,
renderFunction: (Props, any) => React$Node,
props: Props,
currentDispatcher: ?CurrentDispatcherRef,
legacyContext: any,
): HooksTree {
// DevTools will pass the current renderer's injected dispatcher.
// Other apps might compile debug hooks as part of their app though.
Expand All @@ -584,7 +585,11 @@ export function inspectHooks<Props>(
let ancestorStackError;
try {
ancestorStackError = new Error();
renderFunction(props);
if (legacyContext) {
renderFunction(props, legacyContext);
} else {
renderFunction(props);
}
} finally {
readHookLog = hookLog;
hookLog = [];
Expand All @@ -594,7 +599,22 @@ export function inspectHooks<Props>(
return buildTree(rootStack, readHookLog);
}

function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
function getLegacyContext(fiber: Fiber): Object | null {
const hasLegacyContext = !!fiber.elementType.contextTypes;
if (hasLegacyContext) {
let current = fiber;
while (current !== null) {
const instance = current.stateNode;
if (instance && instance.__reactInternalMemoizedMergedChildContext) {
return instance.__reactInternalMemoizedMergedChildContext;
}
current = current.return;
}
}
return null;
}

function setupContext(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
let current = fiber;
while (current) {
if (current.tag === ContextProvider) {
Expand Down Expand Up @@ -686,16 +706,27 @@ export function inspectHooksOfFiber(
currentHook = (fiber.memoizedState: Hook);
const contextMap = new Map();
try {
setupContexts(contextMap, fiber);
if (fiber.tag === ForwardRef) {
if (fiber.tag === FunctionComponent) {
const hasLegacyContext = !!fiber.elementType.contextTypes;
if (hasLegacyContext) {
const legacyContext = getLegacyContext(fiber);
return inspectHooks(type, props, currentDispatcher, legacyContext);
} else {
setupContext(contextMap, fiber);
return inspectHooks(type, props, currentDispatcher);
}
} else if (fiber.tag === ForwardRef) {
setupContext(contextMap, fiber);
return inspectHooksOfForwardRef(
type.render,
props,
fiber.ref,
currentDispatcher,
);
} else {
setupContext(contextMap, fiber);
return inspectHooks(type, props, currentDispatcher);
}
return inspectHooks(type, props, currentDispatcher);
} finally {
currentHook = null;
restoreContexts(contextMap);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
'use strict';

let React;
let PropTypes;
let ReactTestRenderer;
let Scheduler;
let ReactDebugTools;
Expand All @@ -20,6 +21,7 @@ describe('ReactHooksInspectionIntegration', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
PropTypes = require('prop-types');
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
act = ReactTestRenderer.act;
Expand Down Expand Up @@ -335,6 +337,62 @@ describe('ReactHooksInspectionIntegration', () => {
]);
});

it('should be able to access functional legacy context during hook inspection', () => {
let contextValue;

class FirstLegacyContextProvider extends React.Component<any> {
static childContextTypes = {
string: PropTypes.string,
};
getChildContext() {
return {
string: 'valid context',
};
}
render() {
return this.props.children;
}
}

class SecondLegacyContextProvider extends React.Component<any> {
static childContextTypes = {
string: PropTypes.string,
};
getChildContext() {
return {
string: 'invalid context',
};
}
render() {
return this.props.children;
}
}

function FunctionalLegacyContextConsumer(props, context) {
contextValue = context.string;
return <div>{context.string}</div>;
}
FunctionalLegacyContextConsumer.contextTypes = {
string: PropTypes.string,
};

const renderer = ReactTestRenderer.create(
<SecondLegacyContextProvider>
<FirstLegacyContextProvider>
<div>
<FunctionalLegacyContextConsumer />
</div>
</FirstLegacyContextProvider>
</SecondLegacyContextProvider>,
);

const childFiber = renderer.root
.findByType(FunctionalLegacyContextConsumer)
._currentFiber();
expect(() => ReactDebugTools.inspectHooksOfFiber(childFiber)).not.toThrow();
expect(contextValue).toBe('valid context');
});

it('should inspect custom hooks', () => {
function useCustom() {
const [value] = React.useState('hello');
Expand Down
18 changes: 18 additions & 0 deletions packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ type ReactTypeOfSideEffectType = {|
|};

// Some environments (e.g. React Native / Hermes) don't support the performance API yet.

const getCurrentTime =
typeof performance === 'object' && typeof performance.now === 'function'
? () => performance.now()
Expand Down Expand Up @@ -2250,6 +2251,23 @@ export function attach(
if (!shouldHideContext) {
context = stateNode.context;
}
} else if (tag === FunctionComponent) {
const hasLegacyContext = !!fiber.elementType.contextTypes;
if (hasLegacyContext) {
let current = fiber.return;
let childContextFound = false;
while (current !== null && childContextFound === false) {
const instance = current.stateNode;
if (
instance &&
instance.__reactInternalMemoizedMergedChildContext
) {
childContextFound = true;
context = instance.__reactInternalMemoizedMergedChildContext;
}
current = current.return;
}
}
}
} else if (
typeSymbol === CONTEXT_NUMBER ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ class LegacyContextConsumer extends Component<any> {
}
}

function FunctionalLegacyContextConsumer(props, context) {
return null;
}

FunctionalLegacyContextConsumer.contextTypes = {
null: PropTypes.any,
};

const ModernContext = createContext();
ModernContext.displayName = 'ModernContext';
const ArrayContext = createContext(contextData.array);
Expand Down Expand Up @@ -92,22 +100,33 @@ class ModernContextType extends Component<any> {
}
}

function FunctionalContextConsumer() {
function FunctionalModernContextConsumer() {
useContext(StringContext);
return null;
}

function FunctionalLegacyAndModernContextConsumer(props, context) {
useContext(StringContext);
return null;
}

FunctionalLegacyAndModernContextConsumer.contextTypes = {
null: PropTypes.any,
};

export default function Contexts() {
return (
<Fragment>
<LegacyContextProvider>
<LegacyContextConsumer />
<FunctionalLegacyContextConsumer />
<FunctionalLegacyAndModernContextConsumer />
</LegacyContextProvider>
<ModernContext.Provider value={contextData}>
<ModernContext.Consumer>{value => null}</ModernContext.Consumer>
<ModernContextType />
</ModernContext.Provider>
<FunctionalContextConsumer />
<FunctionalModernContextConsumer />
<ArrayContext.Consumer>{value => null}</ArrayContext.Consumer>
<BoolContext.Consumer>{value => null}</BoolContext.Consumer>
<FuncContext.Consumer>{value => null}</FuncContext.Consumer>
Expand Down