- useLayoutEffect(() => {
- if (
- typeof window.getComputedStyle !== 'function' ||
- inputRef.current === null
- ) {
- return;
- }
-
- const inputStyle = window.getComputedStyle(inputRef.current);
- if (!inputStyle) {
- return;
- }
-
- if (hiddenDivRef.current !== null) {
- const divStyle = hiddenDivRef.current.style;
- divStyle.border = inputStyle.border;
- divStyle.fontFamily = inputStyle.fontFamily;
- divStyle.fontSize = inputStyle.fontSize;
- divStyle.fontStyle = inputStyle.fontStyle;
- divStyle.fontWeight = inputStyle.fontWeight;
- divStyle.letterSpacing = inputStyle.letterSpacing;
- divStyle.padding = inputStyle.padding;
- }
- }, []);
-
- // Resize input any time text changes
- useLayoutEffect(() => {
- if (hiddenDivRef.current === null) {
- return;
- }
-
- const scrollWidth = hiddenDivRef.current.getBoundingClientRect().width;
- if (!scrollWidth) {
- return;
- }
-
- // Adding an extra pixel avoids a slight horizontal scroll when changing text selection/cursor.
- // Not sure why this is, but the old DevTools did a similar thing.
- const targetWidth = Math.ceil(scrollWidth) + 1;
-
- if (inputRef.current !== null) {
- inputRef.current.style.width = `${targetWidth}px`;
- }
- }, [value]);
-
const isEmpty = value === '' || value === '""';
return (
-
-
-
- {isEmpty ? placeholder : value}
-
-
+
);
}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NewArrayValue.css b/packages/react-devtools-shared/src/devtools/views/Components/NewArrayValue.css
new file mode 100644
index 0000000000000..5adf42ee11909
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/NewArrayValue.css
@@ -0,0 +1,19 @@
+.NewArrayValue {
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+}
+
+.EditableName {
+ margin-left: 1rem;
+}
+
+.Invalid {
+ background-color: var(--color-background-invalid);
+ color: var(--color-text-invalid) !important;
+}
+
+.Input:focus,
+.Invalid:focus {
+ background-color: var(--color-button-background-focus);
+}
\ No newline at end of file
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NewArrayValue.js b/packages/react-devtools-shared/src/devtools/views/Components/NewArrayValue.js
new file mode 100644
index 0000000000000..cf931e5fea454
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/NewArrayValue.js
@@ -0,0 +1,107 @@
+/**
+ * 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
+ */
+
+import * as React from 'react';
+import {useState} from 'react';
+import Store from '../../store';
+import EditableName from './EditableName';
+import {smartParse} from '../../utils';
+import {parseHookPathForEdit} from './utils';
+import styles from './NewArrayValue.css';
+
+import type {InspectedElement} from './types';
+import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
+
+type Props = {|
+ bridge: FrontendBridge,
+ depth: number,
+ hidden: boolean,
+ hookID?: ?number,
+ index: number,
+ inspectedElement: InspectedElement,
+ path: Array
,
+ store: Store,
+ type: 'props' | 'context' | 'hooks' | 'state',
+|};
+
+export default function NewArrayValue({
+ bridge,
+ depth,
+ hidden,
+ hookID,
+ index,
+ inspectedElement,
+ path,
+ store,
+ type,
+}: Props) {
+ const [key, setKey] = useState(0);
+ const [isInvalid, setIsInvalid] = useState(false);
+
+ // This is a bit of an unusual usage of the EditableName component,
+ // but otherwise it acts the way we want for a new Array entry.
+ const overrideName = (oldPath, newPath) => {
+ const value = newPath[newPath.length - 1];
+
+ let parsedValue;
+ let newIsInvalid = true;
+ try {
+ parsedValue = smartParse(value);
+ newIsInvalid = false;
+ } catch (error) {}
+
+ if (isInvalid !== newIsInvalid) {
+ setIsInvalid(newIsInvalid);
+ }
+
+ if (!newIsInvalid) {
+ setKey(key + 1);
+
+ const {id} = inspectedElement;
+ const rendererID = store.getRendererIDForElement(id);
+ if (rendererID !== null) {
+ let basePath = path;
+ if (hookID != null) {
+ basePath = parseHookPathForEdit(basePath);
+ }
+
+ bridge.send('overrideValueAtPath', {
+ type,
+ hookID,
+ id,
+ path: [...basePath, index],
+ rendererID,
+ value: parsedValue,
+ });
+ }
+ }
+ };
+
+ return (
+
+
+ 0}
+ className={[styles.EditableName, isInvalid && styles.Invalid].join(
+ ' ',
+ )}
+ initialValue=""
+ overrideName={overrideName}
+ path={path}
+ />
+
+
+ );
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NewKeyValue.css b/packages/react-devtools-shared/src/devtools/views/Components/NewKeyValue.css
new file mode 100644
index 0000000000000..e17e5188578a3
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/NewKeyValue.css
@@ -0,0 +1,13 @@
+.NewKeyValue {
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+}
+
+.EditableName {
+ margin-left: 1rem;
+}
+
+.EditableValue {
+ min-width: 1rem;
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NewKeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/NewKeyValue.js
new file mode 100644
index 0000000000000..a5591f0876cb1
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/NewKeyValue.js
@@ -0,0 +1,100 @@
+/**
+ * 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
+ */
+
+import * as React from 'react';
+import {useState} from 'react';
+import Store from '../../store';
+import EditableName from './EditableName';
+import EditableValue from './EditableValue';
+import {parseHookPathForEdit} from './utils';
+import styles from './NewKeyValue.css';
+
+import type {InspectedElement} from './types';
+import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
+
+type Props = {|
+ bridge: FrontendBridge,
+ depth: number,
+ hidden: boolean,
+ hookID?: ?number,
+ inspectedElement: InspectedElement,
+ path: Array,
+ store: Store,
+ type: 'props' | 'state' | 'hooks' | 'context',
+|};
+
+export default function NewKeyValue({
+ bridge,
+ depth,
+ hidden,
+ hookID,
+ inspectedElement,
+ path,
+ store,
+ type,
+}: Props) {
+ const [newPropKey, setNewPropKey] = useState(0);
+ const [newPropName, setNewPropName] = useState('');
+
+ const overrideNewEntryName = (oldPath, newPath) => {
+ setNewPropName(newPath[newPath.length - 1]);
+ };
+
+ const overrideNewEntryValue = (newPath, value) => {
+ if (!newPropName) {
+ return;
+ }
+
+ setNewPropName('');
+ setNewPropKey(newPropKey + 1);
+
+ const {id} = inspectedElement;
+ const rendererID = store.getRendererIDForElement(id);
+ if (rendererID !== null) {
+ let basePath = newPath;
+ if (hookID != null) {
+ basePath = parseHookPathForEdit(basePath);
+ }
+
+ bridge.send('overrideValueAtPath', {
+ type,
+ hookID,
+ id,
+ path: basePath,
+ rendererID,
+ value,
+ });
+ }
+ };
+
+ return (
+
+
+ 0}
+ className={styles.EditableName}
+ overrideName={overrideNewEntryName}
+ path={[]}
+ />
+ :
+
+
+
+ );
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js
deleted file mode 100644
index c8d99bf19c122..0000000000000
--- a/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js
+++ /dev/null
@@ -1,616 +0,0 @@
-/**
- * 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
- */
-
-import {copy} from 'clipboard-js';
-import * as React from 'react';
-import {Fragment, useCallback, useContext} from 'react';
-import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
-import {BridgeContext, ContextMenuContext, StoreContext} from '../context';
-import ContextMenu from '../../ContextMenu/ContextMenu';
-import ContextMenuItem from '../../ContextMenu/ContextMenuItem';
-import Button from '../Button';
-import ButtonIcon from '../ButtonIcon';
-import Icon from '../Icon';
-import HooksTree from './HooksTree';
-import {ModalDialogContext} from '../ModalDialog';
-import HocBadges from './HocBadges';
-import InspectedElementTree from './InspectedElementTree';
-import {InspectedElementContext} from './InspectedElementContext';
-import ViewElementSourceContext from './ViewElementSourceContext';
-import NativeStyleEditor from './NativeStyleEditor';
-import Toggle from '../Toggle';
-import Badge from './Badge';
-import {useHighlightNativeElement} from '../hooks';
-import {
- ComponentFilterElementType,
- ElementTypeClass,
- ElementTypeForwardRef,
- ElementTypeFunction,
- ElementTypeMemo,
- ElementTypeSuspense,
-} from 'react-devtools-shared/src/types';
-
-import styles from './SelectedElement.css';
-
-import type {ContextMenuContextType} from '../context';
-import type {
- CopyInspectedElementPath,
- GetInspectedElementPath,
- InspectedElementContextType,
- StoreAsGlobal,
-} from './InspectedElementContext';
-import type {Element, InspectedElement, Owner} from './types';
-import type {ElementType} from 'react-devtools-shared/src/types';
-
-export type Props = {||};
-
-export default function SelectedElement(_: Props) {
- const {inspectedElementID} = useContext(TreeStateContext);
- const dispatch = useContext(TreeDispatcherContext);
- const {canViewElementSourceFunction, viewElementSourceFunction} = useContext(
- ViewElementSourceContext,
- );
- const bridge = useContext(BridgeContext);
- const store = useContext(StoreContext);
- const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
-
- const {
- copyInspectedElementPath,
- getInspectedElementPath,
- getInspectedElement,
- storeAsGlobal,
- } = useContext(InspectedElementContext);
-
- const element =
- inspectedElementID !== null
- ? store.getElementByID(inspectedElementID)
- : null;
-
- const inspectedElement =
- inspectedElementID != null ? getInspectedElement(inspectedElementID) : null;
-
- const highlightElement = useCallback(() => {
- if (element !== null && inspectedElementID !== null) {
- const rendererID = store.getRendererIDForElement(inspectedElementID);
- if (rendererID !== null) {
- bridge.send('highlightNativeElement', {
- displayName: element.displayName,
- hideAfterTimeout: true,
- id: inspectedElementID,
- openNativeElementsPanel: true,
- rendererID,
- scrollIntoView: true,
- });
- }
- }
- }, [bridge, element, inspectedElementID, store]);
-
- const logElement = useCallback(() => {
- if (inspectedElementID !== null) {
- const rendererID = store.getRendererIDForElement(inspectedElementID);
- if (rendererID !== null) {
- bridge.send('logElementToConsole', {
- id: inspectedElementID,
- rendererID,
- });
- }
- }
- }, [bridge, inspectedElementID, store]);
-
- const viewSource = useCallback(() => {
- if (viewElementSourceFunction != null && inspectedElement !== null) {
- viewElementSourceFunction(
- inspectedElement.id,
- ((inspectedElement: any): InspectedElement),
- );
- }
- }, [inspectedElement, viewElementSourceFunction]);
-
- // In some cases (e.g. FB internal usage) the standalone shell might not be able to view the source.
- // To detect this case, we defer to an injected helper function (if present).
- const canViewSource =
- inspectedElement !== null &&
- inspectedElement.canViewSource &&
- viewElementSourceFunction !== null &&
- (canViewElementSourceFunction === null ||
- canViewElementSourceFunction(inspectedElement));
-
- const isSuspended =
- element !== null &&
- element.type === ElementTypeSuspense &&
- inspectedElement != null &&
- inspectedElement.state != null;
-
- const canToggleSuspense =
- inspectedElement != null && inspectedElement.canToggleSuspense;
-
- // TODO (suspense toggle) Would be nice to eventually use a two setState pattern here as well.
- const toggleSuspended = useCallback(() => {
- let nearestSuspenseElement = null;
- let currentElement = element;
- while (currentElement !== null) {
- if (currentElement.type === ElementTypeSuspense) {
- nearestSuspenseElement = currentElement;
- break;
- } else if (currentElement.parentID > 0) {
- currentElement = store.getElementByID(currentElement.parentID);
- } else {
- currentElement = null;
- }
- }
-
- // If we didn't find a Suspense ancestor, we can't suspend.
- // Instead we can show a warning to the user.
- if (nearestSuspenseElement === null) {
- modalDialogDispatch({
- type: 'SHOW',
- content: ,
- });
- } else {
- const nearestSuspenseElementID = nearestSuspenseElement.id;
-
- // If we're suspending from an arbitrary (non-Suspense) component, select the nearest Suspense element in the Tree.
- // This way when the fallback UI is shown and the current element is hidden, something meaningful is selected.
- if (nearestSuspenseElement !== element) {
- dispatch({
- type: 'SELECT_ELEMENT_BY_ID',
- payload: nearestSuspenseElementID,
- });
- }
-
- const rendererID = store.getRendererIDForElement(
- nearestSuspenseElementID,
- );
-
- // Toggle suspended
- if (rendererID !== null) {
- bridge.send('overrideSuspense', {
- id: nearestSuspenseElementID,
- rendererID,
- forceFallback: !isSuspended,
- });
- }
- }
- }, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store]);
-
- if (element === null) {
- return (
-
- );
- }
-
- return (
-
-
- {element.key && (
- <>
-
- {element.key}
-
-
- >
- )}
-
-
-
- {element.displayName}
-
-
-
- {canToggleSuspense && (
-
-
-
- )}
- {store.supportsNativeInspection && (
-
-
-
- )}
-
-
-
-
-
-
-
-
- {inspectedElement === null && (
-
Loading...
- )}
-
- {inspectedElement !== null && (
-
- )}
-
- );
-}
-
-export type CopyPath = (path: Array) => void;
-export type InspectPath = (path: Array) => void;
-
-type InspectedElementViewProps = {|
- copyInspectedElementPath: CopyInspectedElementPath,
- element: Element,
- getInspectedElementPath: GetInspectedElementPath,
- inspectedElement: InspectedElement,
- storeAsGlobal: StoreAsGlobal,
-|};
-
-const IS_SUSPENDED = 'Suspended';
-
-function InspectedElementView({
- copyInspectedElementPath,
- element,
- getInspectedElementPath,
- inspectedElement,
- storeAsGlobal,
-}: InspectedElementViewProps) {
- const {id, type} = element;
- const {
- canEditFunctionProps,
- canEditHooks,
- canToggleSuspense,
- hasLegacyContext,
- context,
- hooks,
- owners,
- props,
- rendererPackageName,
- rendererVersion,
- rootType,
- source,
- state,
- } = inspectedElement;
-
- const bridge = useContext(BridgeContext);
- const store = useContext(StoreContext);
-
- const {
- isEnabledForInspectedElement,
- viewAttributeSourceFunction,
- } = useContext(ContextMenuContext);
-
- const inspectContextPath = useCallback(
- (path: Array) => {
- getInspectedElementPath(id, ['context', ...path]);
- },
- [getInspectedElementPath, id],
- );
- const inspectPropsPath = useCallback(
- (path: Array) => {
- getInspectedElementPath(id, ['props', ...path]);
- },
- [getInspectedElementPath, id],
- );
- const inspectStatePath = useCallback(
- (path: Array) => {
- getInspectedElementPath(id, ['state', ...path]);
- },
- [getInspectedElementPath, id],
- );
-
- let overrideContextFn = null;
- let overridePropsFn = null;
- let overrideStateFn = null;
- let overrideSuspenseFn = null;
- if (type === ElementTypeClass) {
- overrideContextFn = (path: Array, value: any) => {
- const rendererID = store.getRendererIDForElement(id);
- if (rendererID !== null) {
- bridge.send('overrideContext', {id, path, rendererID, value});
- }
- };
- overridePropsFn = (path: Array, value: any) => {
- const rendererID = store.getRendererIDForElement(id);
- if (rendererID !== null) {
- bridge.send('overrideProps', {id, path, rendererID, value});
- }
- };
- overrideStateFn = (path: Array, value: any) => {
- const rendererID = store.getRendererIDForElement(id);
- if (rendererID !== null) {
- bridge.send('overrideState', {id, path, rendererID, value});
- }
- };
- } else if (
- (type === ElementTypeFunction ||
- type === ElementTypeMemo ||
- type === ElementTypeForwardRef) &&
- canEditFunctionProps
- ) {
- overridePropsFn = (path: Array, value: any) => {
- const rendererID = store.getRendererIDForElement(id);
- if (rendererID !== null) {
- bridge.send('overrideProps', {id, path, rendererID, value});
- }
- };
- } else if (type === ElementTypeSuspense && canToggleSuspense) {
- overrideSuspenseFn = (path: Array, value: boolean) => {
- if (path.length !== 1 && path !== IS_SUSPENDED) {
- throw new Error('Unexpected path.');
- }
- const rendererID = store.getRendererIDForElement(id);
- if (rendererID !== null) {
- bridge.send('overrideSuspense', {
- id,
- rendererID,
- forceFallback: value,
- });
- }
- };
- }
-
- const rendererLabel =
- rendererPackageName !== null && rendererVersion !== null
- ? `${rendererPackageName}@${rendererVersion}`
- : null;
- const showOwnersList = owners !== null && owners.length > 0;
- const showRenderedBy =
- showOwnersList || rendererLabel !== null || rootType !== null;
-
- return (
-
-
-
-
- {type === ElementTypeSuspense ? (
-
- ) : (
-
- )}
-
-
-
-
-
- {showRenderedBy && (
-
-
rendered by
- {showOwnersList &&
- ((owners: any): Array
).map(owner => (
-
- ))}
- {rootType !== null && (
- {rootType}
- )}
- {rendererLabel !== null && (
- {rendererLabel}
- )}
-
- )}
-
- {source !== null && (
-
- )}
-
-
- {isEnabledForInspectedElement && (
-
- {data => (
-
- copyInspectedElementPath(id, data.path)}
- title="Copy value to clipboard">
- Copy
- value to clipboard
-
- storeAsGlobal(id, data.path)}
- title="Store as global variable">
- {' '}
- Store as global variable
-
- {viewAttributeSourceFunction !== null &&
- data.type === 'function' && (
- viewAttributeSourceFunction(id, data.path)}
- title="Go to definition">
- Go
- to definition
-
- )}
-
- )}
-
- )}
-
- );
-}
-
-// This function is based on describeComponentFrame() in packages/shared/ReactComponentStackFrame
-function formatSourceForDisplay(fileName: string, lineNumber: string) {
- const BEFORE_SLASH_RE = /^(.*)[\\\/]/;
-
- let nameOnly = fileName.replace(BEFORE_SLASH_RE, '');
-
- // In DEV, include code for a common special case:
- // prefer "folder/index.js" instead of just "index.js".
- if (/^index\./.test(nameOnly)) {
- const match = fileName.match(BEFORE_SLASH_RE);
- if (match) {
- const pathBeforeSlash = match[1];
- if (pathBeforeSlash) {
- const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
- nameOnly = folderName + '/' + nameOnly;
- }
- }
- }
-
- return `${nameOnly}:${lineNumber}`;
-}
-
-type SourceProps = {|
- fileName: string,
- lineNumber: string,
-|};
-
-function Source({fileName, lineNumber}: SourceProps) {
- const handleCopy = () => copy(`${fileName}:${lineNumber}`);
- return (
-
-
-
- {formatSourceForDisplay(fileName, lineNumber)}
-
-
- );
-}
-
-type OwnerViewProps = {|
- displayName: string,
- hocDisplayNames: Array | null,
- id: number,
- isInStore: boolean,
- type: ElementType,
-|};
-
-function OwnerView({
- displayName,
- hocDisplayNames,
- id,
- isInStore,
- type,
-}: OwnerViewProps) {
- const dispatch = useContext(TreeDispatcherContext);
- const {
- highlightNativeElement,
- clearHighlightNativeElement,
- } = useHighlightNativeElement();
-
- const handleClick = useCallback(
- () =>
- dispatch({
- type: 'SELECT_ELEMENT_BY_ID',
- payload: id,
- }),
- [dispatch, id],
- );
-
- const onMouseEnter = () => highlightNativeElement(id);
-
- const onMouseLeave = clearHighlightNativeElement;
-
- return (
-
-
-
- {displayName}
-
-
-
-
- );
-}
-
-function CannotSuspendWarningMessage() {
- const store = useContext(StoreContext);
- const areSuspenseElementsHidden = !!store.componentFilters.find(
- filter =>
- filter.type === ComponentFilterElementType &&
- filter.value === ElementTypeSuspense &&
- filter.isEnabled,
- );
-
- // Has the user filtered out Suspense nodes from the tree?
- // If so, the selected element might actually be in a Suspense tree after all.
- if (areSuspenseElementsHidden) {
- return (
-
- Suspended state cannot be toggled while Suspense components are hidden.
- Disable the filter and try again.
-
- );
- } else {
- return (
-
- The selected element is not within a Suspense container. Suspending it
- would cause an error.
-
- );
- }
-}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/types.js b/packages/react-devtools-shared/src/devtools/views/Components/types.js
index 93ccd70e865fa..cccaba1c9b151 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/types.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/types.js
@@ -59,12 +59,16 @@ export type OwnersList = {|
export type InspectedElement = {|
id: number,
- // Does the current renderer support editable hooks?
+ // Does the current renderer support editable hooks and function props?
canEditHooks: boolean,
-
- // Does the current renderer support editable function props?
canEditFunctionProps: boolean,
+ // Does the current renderer support advanced editing interface?
+ canEditHooksAndDeletePaths: boolean,
+ canEditHooksAndRenamePaths: boolean,
+ canEditFunctionPropsDeletePaths: boolean,
+ canEditFunctionPropsRenamePaths: boolean,
+
// Is this Suspense, and can its value be overridden now?
canToggleSuspense: boolean,
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/utils.js b/packages/react-devtools-shared/src/devtools/views/Components/utils.js
new file mode 100644
index 0000000000000..0197e580ce765
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/utils.js
@@ -0,0 +1,28 @@
+/**
+ * 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
+ */
+
+/**
+ * Converts nested hooks paths to the format expected by the backend.
+ * e.g. [''] => ['']
+ * e.g. [1, 'value', ...] => [...]
+ * e.g. [2, 'subhooks', 1, 'value', ...] => [...]
+ * e.g. [1, 'subhooks', 3, 'subhooks', 2, 'value', ...] => [...]
+ */
+export function parseHookPathForEdit(
+ path: Array,
+): Array {
+ let index = 0;
+ for (let i = 0; i < path.length; i++) {
+ if (path[i] === 'value') {
+ index = i + 1;
+ break;
+ }
+ }
+ return path.slice(index);
+}
diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js
index a0ec1038e75dc..088df58d6f7ec 100644
--- a/packages/react-devtools-shared/src/utils.js
+++ b/packages/react-devtools-shared/src/utils.js
@@ -374,6 +374,45 @@ export function getInObject(object: Object, path: Array): any {
}, object);
}
+export function deletePathInObject(
+ object: Object,
+ path: Array,
+) {
+ const length = path.length;
+ const last = path[length - 1];
+ if (object != null) {
+ const parent = getInObject(object, path.slice(0, length - 1));
+ if (parent) {
+ if (Array.isArray(parent)) {
+ parent.splice(((last: any): number), 1);
+ } else {
+ delete parent[last];
+ }
+ }
+ }
+}
+
+export function renamePathInObject(
+ object: Object,
+ oldPath: Array,
+ newPath: Array,
+) {
+ const length = oldPath.length;
+ if (object != null) {
+ const parent = getInObject(object, oldPath.slice(0, length - 1));
+ if (parent) {
+ const lastOld = oldPath[length - 1];
+ const lastNew = newPath[length - 1];
+ parent[lastNew] = parent[lastOld];
+ if (Array.isArray(parent)) {
+ parent.splice(((lastOld: any): number), 1);
+ } else {
+ delete parent[lastOld];
+ }
+ }
+ }
+}
+
export function setInObject(
object: Object,
path: Array,
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js
index 6d514d2f2a369..86749f5c2039d 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.new.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js
@@ -466,24 +466,106 @@ export function shouldSuspend(fiber: Fiber): boolean {
}
let overrideHookState = null;
+let overrideHookStateDeletePath = null;
+let overrideHookStateRenamePath = null;
let overrideProps = null;
+let overridePropsDeletePath = null;
+let overridePropsRenamePath = null;
let scheduleUpdate = null;
let setSuspenseHandler = null;
if (__DEV__) {
+ const copyWithDeleteImpl = (
+ obj: Object | Array,
+ path: Array,
+ index: number,
+ ) => {
+ const key = path[index];
+ const updated = Array.isArray(obj) ? obj.slice() : {...obj};
+ if (index + 1 === path.length) {
+ if (Array.isArray(updated)) {
+ updated.splice(((key: any): number), 1);
+ } else {
+ delete updated[key];
+ }
+ return updated;
+ }
+ // $FlowFixMe number or string is fine here
+ updated[key] = copyWithDeleteImpl(obj[key], path, index + 1);
+ return updated;
+ };
+
+ const copyWithDelete = (
+ obj: Object | Array,
+ path: Array,
+ ): Object | Array => {
+ return copyWithDeleteImpl(obj, path, 0);
+ };
+
+ const copyWithRenameImpl = (
+ obj: Object | Array,
+ oldPath: Array,
+ newPath: Array,
+ index: number,
+ ) => {
+ const oldKey = oldPath[index];
+ const updated = Array.isArray(obj) ? obj.slice() : {...obj};
+ if (index + 1 === oldPath.length) {
+ const newKey = newPath[index];
+ // $FlowFixMe number or string is fine here
+ updated[newKey] = updated[oldKey];
+ if (Array.isArray(updated)) {
+ updated.splice(((oldKey: any): number), 1);
+ } else {
+ delete updated[oldKey];
+ }
+ } else {
+ // $FlowFixMe number or string is fine here
+ updated[oldKey] = copyWithRenameImpl(
+ // $FlowFixMe number or string is fine here
+ obj[oldKey],
+ oldPath,
+ newPath,
+ index + 1,
+ );
+ }
+ return updated;
+ };
+
+ const copyWithRename = (
+ obj: Object | Array,
+ oldPath: Array,
+ newPath: Array,
+ ): Object | Array => {
+ if (oldPath.length !== newPath.length) {
+ console.warn('copyWithRename() expects paths of the same length');
+ return;
+ } else {
+ for (let i = 0; i < newPath.length - 1; i++) {
+ if (oldPath[i] !== newPath[i]) {
+ console.warn(
+ 'copyWithRename() expects paths to be the same except for the deepest key',
+ );
+ return;
+ }
+ }
+ }
+ return copyWithRenameImpl(obj, oldPath, newPath, 0);
+ };
+
const copyWithSetImpl = (
obj: Object | Array,
path: Array,
- idx: number,
+ index: number,
value: any,
) => {
- if (idx >= path.length) {
+ if (index >= path.length) {
return value;
}
- const key = path[idx];
+ const key = path[index];
const updated = Array.isArray(obj) ? obj.slice() : {...obj};
// $FlowFixMe number or string is fine here
- updated[key] = copyWithSetImpl(obj[key], path, idx + 1, value);
+ updated[key] = copyWithSetImpl(obj[key], path, index + 1, value);
return updated;
};
@@ -495,6 +577,17 @@ if (__DEV__) {
return copyWithSetImpl(obj, path, 0, value);
};
+ const findHook = (fiber: Fiber, id: number) => {
+ // For now, the "id" of stateful hooks is just the stateful hook index.
+ // This may change in the future with e.g. nested hooks.
+ let currentHook = fiber.memoizedState;
+ while (currentHook !== null && id > 0) {
+ currentHook = currentHook.next;
+ id--;
+ }
+ return currentHook;
+ };
+
// Support DevTools editable values for useState and useReducer.
overrideHookState = (
fiber: Fiber,
@@ -502,17 +595,54 @@ if (__DEV__) {
path: Array,
value: any,
) => {
- // For now, the "id" of stateful hooks is just the stateful hook index.
- // This may change in the future with e.g. nested hooks.
- let currentHook = fiber.memoizedState;
- while (currentHook !== null && id > 0) {
- currentHook = currentHook.next;
- id--;
+ const hook = findHook(fiber, id);
+ if (hook !== null) {
+ const newState = copyWithSet(hook.memoizedState, path, value);
+ hook.memoizedState = newState;
+ hook.baseState = newState;
+
+ // We aren't actually adding an update to the queue,
+ // because there is no update we can add for useReducer hooks that won't trigger an error.
+ // (There's no appropriate action type for DevTools overrides.)
+ // As a result though, React will see the scheduled update as a noop and bailout.
+ // Shallow cloning props works as a workaround for now to bypass the bailout check.
+ fiber.memoizedProps = {...fiber.memoizedProps};
+
+ scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
}
- if (currentHook !== null) {
- const newState = copyWithSet(currentHook.memoizedState, path, value);
- currentHook.memoizedState = newState;
- currentHook.baseState = newState;
+ };
+ overrideHookStateDeletePath = (
+ fiber: Fiber,
+ id: number,
+ path: Array,
+ ) => {
+ const hook = findHook(fiber, id);
+ if (hook !== null) {
+ const newState = copyWithDelete(hook.memoizedState, path);
+ hook.memoizedState = newState;
+ hook.baseState = newState;
+
+ // We aren't actually adding an update to the queue,
+ // because there is no update we can add for useReducer hooks that won't trigger an error.
+ // (There's no appropriate action type for DevTools overrides.)
+ // As a result though, React will see the scheduled update as a noop and bailout.
+ // Shallow cloning props works as a workaround for now to bypass the bailout check.
+ fiber.memoizedProps = {...fiber.memoizedProps};
+
+ scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
+ }
+ };
+ overrideHookStateRenamePath = (
+ fiber: Fiber,
+ id: number,
+ oldPath: Array,
+ newPath: Array,
+ ) => {
+ const hook = findHook(fiber, id);
+ if (hook !== null) {
+ const newState = copyWithRename(hook.memoizedState, oldPath, newPath);
+ hook.memoizedState = newState;
+ hook.baseState = newState;
// We aren't actually adding an update to the queue,
// because there is no update we can add for useReducer hooks that won't trigger an error.
@@ -533,6 +663,24 @@ if (__DEV__) {
}
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
};
+ overridePropsDeletePath = (fiber: Fiber, path: Array) => {
+ fiber.pendingProps = copyWithDelete(fiber.memoizedProps, path);
+ if (fiber.alternate) {
+ fiber.alternate.pendingProps = fiber.pendingProps;
+ }
+ scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
+ };
+ overridePropsRenamePath = (
+ fiber: Fiber,
+ oldPath: Array,
+ newPath: Array,
+ ) => {
+ fiber.pendingProps = copyWithRename(fiber.memoizedProps, oldPath, newPath);
+ if (fiber.alternate) {
+ fiber.alternate.pendingProps = fiber.pendingProps;
+ }
+ scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
+ };
scheduleUpdate = (fiber: Fiber) => {
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
@@ -571,7 +719,11 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
rendererPackageName: devToolsConfig.rendererPackageName,
rendererConfig: devToolsConfig.rendererConfig,
overrideHookState,
+ overrideHookStateDeletePath,
+ overrideHookStateRenamePath,
overrideProps,
+ overridePropsDeletePath,
+ overridePropsRenamePath,
setSuspenseHandler,
scheduleUpdate,
currentDispatcherRef: ReactCurrentDispatcher,
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js
index 5aa381ea4e5cd..425cde8ab84f5 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.old.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js
@@ -466,24 +466,106 @@ export function shouldSuspend(fiber: Fiber): boolean {
}
let overrideHookState = null;
+let overrideHookStateDeletePath = null;
+let overrideHookStateRenamePath = null;
let overrideProps = null;
+let overridePropsDeletePath = null;
+let overridePropsRenamePath = null;
let scheduleUpdate = null;
let setSuspenseHandler = null;
if (__DEV__) {
+ const copyWithDeleteImpl = (
+ obj: Object | Array,
+ path: Array,
+ index: number,
+ ) => {
+ const key = path[index];
+ const updated = Array.isArray(obj) ? obj.slice() : {...obj};
+ if (index + 1 === path.length) {
+ if (Array.isArray(updated)) {
+ updated.splice(((key: any): number), 1);
+ } else {
+ delete updated[key];
+ }
+ return updated;
+ }
+ // $FlowFixMe number or string is fine here
+ updated[key] = copyWithDeleteImpl(obj[key], path, index + 1);
+ return updated;
+ };
+
+ const copyWithDelete = (
+ obj: Object | Array,
+ path: Array,
+ ): Object | Array => {
+ return copyWithDeleteImpl(obj, path, 0);
+ };
+
+ const copyWithRenameImpl = (
+ obj: Object | Array,
+ oldPath: Array,
+ newPath: Array,
+ index: number,
+ ) => {
+ const oldKey = oldPath[index];
+ const updated = Array.isArray(obj) ? obj.slice() : {...obj};
+ if (index + 1 === oldPath.length) {
+ const newKey = newPath[index];
+ // $FlowFixMe number or string is fine here
+ updated[newKey] = updated[oldKey];
+ if (Array.isArray(updated)) {
+ updated.splice(((oldKey: any): number), 1);
+ } else {
+ delete updated[oldKey];
+ }
+ } else {
+ // $FlowFixMe number or string is fine here
+ updated[oldKey] = copyWithRenameImpl(
+ // $FlowFixMe number or string is fine here
+ obj[oldKey],
+ oldPath,
+ newPath,
+ index + 1,
+ );
+ }
+ return updated;
+ };
+
+ const copyWithRename = (
+ obj: Object | Array,
+ oldPath: Array,
+ newPath: Array,
+ ): Object | Array => {
+ if (oldPath.length !== newPath.length) {
+ console.warn('copyWithRename() expects paths of the same length');
+ return;
+ } else {
+ for (let i = 0; i < newPath.length - 1; i++) {
+ if (oldPath[i] !== newPath[i]) {
+ console.warn(
+ 'copyWithRename() expects paths to be the same except for the deepest key',
+ );
+ return;
+ }
+ }
+ }
+ return copyWithRenameImpl(obj, oldPath, newPath, 0);
+ };
+
const copyWithSetImpl = (
obj: Object | Array,
path: Array,
- idx: number,
+ index: number,
value: any,
) => {
- if (idx >= path.length) {
+ if (index >= path.length) {
return value;
}
- const key = path[idx];
+ const key = path[index];
const updated = Array.isArray(obj) ? obj.slice() : {...obj};
// $FlowFixMe number or string is fine here
- updated[key] = copyWithSetImpl(obj[key], path, idx + 1, value);
+ updated[key] = copyWithSetImpl(obj[key], path, index + 1, value);
return updated;
};
@@ -495,6 +577,17 @@ if (__DEV__) {
return copyWithSetImpl(obj, path, 0, value);
};
+ const findHook = (fiber: Fiber, id: number) => {
+ // For now, the "id" of stateful hooks is just the stateful hook index.
+ // This may change in the future with e.g. nested hooks.
+ let currentHook = fiber.memoizedState;
+ while (currentHook !== null && id > 0) {
+ currentHook = currentHook.next;
+ id--;
+ }
+ return currentHook;
+ };
+
// Support DevTools editable values for useState and useReducer.
overrideHookState = (
fiber: Fiber,
@@ -502,17 +595,54 @@ if (__DEV__) {
path: Array,
value: any,
) => {
- // For now, the "id" of stateful hooks is just the stateful hook index.
- // This may change in the future with e.g. nested hooks.
- let currentHook = fiber.memoizedState;
- while (currentHook !== null && id > 0) {
- currentHook = currentHook.next;
- id--;
+ const hook = findHook(fiber, id);
+ if (hook !== null) {
+ const newState = copyWithSet(hook.memoizedState, path, value);
+ hook.memoizedState = newState;
+ hook.baseState = newState;
+
+ // We aren't actually adding an update to the queue,
+ // because there is no update we can add for useReducer hooks that won't trigger an error.
+ // (There's no appropriate action type for DevTools overrides.)
+ // As a result though, React will see the scheduled update as a noop and bailout.
+ // Shallow cloning props works as a workaround for now to bypass the bailout check.
+ fiber.memoizedProps = {...fiber.memoizedProps};
+
+ scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
}
- if (currentHook !== null) {
- const newState = copyWithSet(currentHook.memoizedState, path, value);
- currentHook.memoizedState = newState;
- currentHook.baseState = newState;
+ };
+ overrideHookStateDeletePath = (
+ fiber: Fiber,
+ id: number,
+ path: Array,
+ ) => {
+ const hook = findHook(fiber, id);
+ if (hook !== null) {
+ const newState = copyWithDelete(hook.memoizedState, path);
+ hook.memoizedState = newState;
+ hook.baseState = newState;
+
+ // We aren't actually adding an update to the queue,
+ // because there is no update we can add for useReducer hooks that won't trigger an error.
+ // (There's no appropriate action type for DevTools overrides.)
+ // As a result though, React will see the scheduled update as a noop and bailout.
+ // Shallow cloning props works as a workaround for now to bypass the bailout check.
+ fiber.memoizedProps = {...fiber.memoizedProps};
+
+ scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
+ }
+ };
+ overrideHookStateRenamePath = (
+ fiber: Fiber,
+ id: number,
+ oldPath: Array,
+ newPath: Array,
+ ) => {
+ const hook = findHook(fiber, id);
+ if (hook !== null) {
+ const newState = copyWithRename(hook.memoizedState, oldPath, newPath);
+ hook.memoizedState = newState;
+ hook.baseState = newState;
// We aren't actually adding an update to the queue,
// because there is no update we can add for useReducer hooks that won't trigger an error.
@@ -533,6 +663,24 @@ if (__DEV__) {
}
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
};
+ overridePropsDeletePath = (fiber: Fiber, path: Array) => {
+ fiber.pendingProps = copyWithDelete(fiber.memoizedProps, path);
+ if (fiber.alternate) {
+ fiber.alternate.pendingProps = fiber.pendingProps;
+ }
+ scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
+ };
+ overridePropsRenamePath = (
+ fiber: Fiber,
+ oldPath: Array,
+ newPath: Array,
+ ) => {
+ fiber.pendingProps = copyWithRename(fiber.memoizedProps, oldPath, newPath);
+ if (fiber.alternate) {
+ fiber.alternate.pendingProps = fiber.pendingProps;
+ }
+ scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
+ };
scheduleUpdate = (fiber: Fiber) => {
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
@@ -571,7 +719,11 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
rendererPackageName: devToolsConfig.rendererPackageName,
rendererConfig: devToolsConfig.rendererConfig,
overrideHookState,
+ overrideHookStateDeletePath,
+ overrideHookStateRenamePath,
overrideProps,
+ overridePropsDeletePath,
+ overridePropsRenamePath,
setSuspenseHandler,
scheduleUpdate,
currentDispatcherRef: ReactCurrentDispatcher,
diff --git a/yarn.lock b/yarn.lock
index ed3c98c27939a..2773784bd70cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8766,7 +8766,7 @@ json5@^2.1.0:
dependencies:
minimist "^1.2.0"
-json5@^2.1.2:
+json5@^2.1.2, json5@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==