Skip to content

feat(devtools): Allow editing of context value in Consumer #18257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
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
99 changes: 65 additions & 34 deletions packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
@@ -2212,6 +2212,39 @@ export function attach(
return {instance, style};
}

// find the provider fiber that provides the value for this particular consumer
function getProviderFiber(consumer: Fiber): Fiber | null {
const type = consumer.type;
// 16.3-16.5 read from "type" because the Consumer is the actual context object.
// 16.6+ should read from "type._context" because Consumer can be different (in DEV).
// NOTE Keep in sync with getDisplayNameForFiber()
const consumerResolvedContext = type._context || type;

// Look for overridden value.
let current = consumer.return;
while (current !== null) {
const currentType = current.type;
const currentTypeSymbol = getTypeSymbol(currentType);
if (
currentTypeSymbol === CONTEXT_PROVIDER_NUMBER ||
currentTypeSymbol === CONTEXT_PROVIDER_SYMBOL_STRING
) {
// 16.3.0 exposed the context object as "context"
// PR #12501 changed it to "_context" for 16.3.1+
// NOTE Keep in sync with getDisplayNameForFiber()
const providerResolvedContext =
currentType._context || currentType.context;
if (providerResolvedContext === consumerResolvedContext) {
return current;
}
}

current = current.return;
}

return null;
}

function inspectElementRaw(id: number): InspectedElement | null {
let fiber = findCurrentFiberUsingSlowPathById(id);
if (fiber == null) {
@@ -2270,30 +2303,12 @@ export function attach(
// NOTE Keep in sync with getDisplayNameForFiber()
const consumerResolvedContext = type._context || type;

const providerFiber = getProviderFiber(fiber);
// Global context value.
context = consumerResolvedContext._currentValue || null;

// Look for overridden value.
let current = ((fiber: any): Fiber).return;
while (current !== null) {
const currentType = current.type;
const currentTypeSymbol = getTypeSymbol(currentType);
if (
currentTypeSymbol === CONTEXT_PROVIDER_NUMBER ||
currentTypeSymbol === CONTEXT_PROVIDER_SYMBOL_STRING
) {
// 16.3.0 exposed the context object as "context"
// PR #12501 changed it to "_context" for 16.3.1+
// NOTE Keep in sync with getDisplayNameForFiber()
const providerResolvedContext =
currentType._context || currentType.context;
if (providerResolvedContext === consumerResolvedContext) {
context = current.memoizedProps.value;
break;
}
}

current = current.return;
if (providerFiber === null) {
context = consumerResolvedContext._currentValue || null;
} else {
context = providerFiber.memoizedProps.value;
}
}

@@ -2715,21 +2730,37 @@ export function attach(
}

function setInContext(id: number, path: Array<string | number>, value: any) {
// To simplify hydration and display of primative context values (e.g. number, string)
// the inspectElement() method wraps context in a {value: ...} object.
// We need to remove the first part of the path (the "value") before continuing.
path = path.slice(1);

const fiber = findCurrentFiberUsingSlowPathById(id);
if (fiber !== null) {
const instance = fiber.stateNode;
if (path.length === 0) {
// Simple context value
instance.context = value;
const typeSymbol = getTypeSymbol(fiber.type);
if (
typeSymbol === CONTEXT_CONSUMER_NUMBER ||
typeSymbol === CONTEXT_CONSUMER_SYMBOL_STRING
) {
// treat this like an update to the `value` prop of a Context.Provider

const providerFiber = getProviderFiber(fiber);

// if the edited context value was the default value of the context
// there is no Context.Provider above the consumer fiber
if (providerFiber !== null && typeof overrideProps === 'function') {
overrideProps(providerFiber, path, value);
}
} else {
setInObject(instance.context, path, value);
// To simplify hydration and display of primative context values (e.g. number, string)
// the inspectElement() method wraps context in a {value: ...} object.
// We need to remove the first part of the path (the "value") before continuing.
path = path.slice(1);

const instance = fiber.stateNode;
if (path.length === 0) {
// Simple context value
instance.context = value;
} else {
setInObject(instance.context, path, value);
}
instance.forceUpdate();
}
instance.forceUpdate();
}
}

Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@ import {
ElementTypeFunction,
ElementTypeMemo,
ElementTypeSuspense,
ElementTypeContext,
} from 'react-devtools-shared/src/types';

import styles from './SelectedElement.css';
@@ -362,6 +363,13 @@ function InspectedElementView({
});
}
};
} else if (type === ElementTypeContext) {
overrideContextFn = (path: Array<string | number>, value: any) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('overrideContext', {id, path, rendererID, value});
}
};
}

return (
Original file line number Diff line number Diff line change
@@ -104,7 +104,23 @@ export default function Contexts() {
<LegacyContextConsumer />
</LegacyContextProvider>
<ModernContext.Provider value={contextData}>
<ModernContext.Consumer>{value => null}</ModernContext.Consumer>
<ModernContext.Consumer>
{value => {
return (
<React.Fragment>
<pre>{JSON.stringify(value, null, 2)}</pre>
<ModernContext.Provider
value={{nested: true, array: ['a', 'b', 'c']}}>
<ModernContext.Consumer>
{nestedValue => (
<pre>{JSON.stringify(nestedValue, null, 2)}</pre>
)}
</ModernContext.Consumer>
</ModernContext.Provider>
</React.Fragment>
);
}}
</ModernContext.Consumer>
<ModernContextType />
</ModernContext.Provider>
<FunctionalContextConsumer />