From c963476acc31cb1c7a3ccfa7249ef44cd3e12375 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Sat, 7 Sep 2019 17:22:12 +0200
Subject: [PATCH 01/15] Extracted sanitizeForParse

---
 .../react-devtools-shared/src/devtools/utils.js    | 13 +++++++++++++
 .../Components/NativeStyleEditor/StyleEditor.js    | 14 +-------------
 2 files changed, 14 insertions(+), 13 deletions(-)

diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js
index aaffcad56e272..6cd23d4dbf3ce 100644
--- a/packages/react-devtools-shared/src/devtools/utils.js
+++ b/packages/react-devtools-shared/src/devtools/utils.js
@@ -82,3 +82,16 @@ export function printStore(store: Store, includeWeight: boolean = false) {
 
   return snapshotLines.join('\n');
 }
+
+// We use JSON.parse to parse string values
+// e.g. 'foo' is not valid JSON but it is a valid string
+// so this method replaces e.g. 'foo' with "foo"
+export function sanitizeForParse(value: any) {
+  if (typeof value === 'string') {
+    if (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'") {
+      return '"' + value.substr(1, value.length - 2) + '"';
+    }
+  }
+
+  return value;
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js
index 2a08e99cf4e71..8e0210d44b49c 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js
@@ -19,6 +19,7 @@ import ButtonIcon from '../../ButtonIcon';
 import {serializeDataForCopy} from '../../utils';
 import AutoSizeInput from './AutoSizeInput';
 import styles from './StyleEditor.css';
+import {sanitizeForParse} from '../../../utils';
 
 import type {Style} from './types';
 
@@ -290,16 +291,3 @@ function Field({
     />
   );
 }
-
-// We use JSON.parse to parse string values
-// e.g. 'foo' is not valid JSON but it is a valid string
-// so this method replaces e.g. 'foo' with "foo"
-function sanitizeForParse(value: any) {
-  if (typeof value === 'string') {
-    if (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'") {
-      return '"' + value.substr(1, value.length - 2) + '"';
-    }
-  }
-
-  return value;
-}

From 4470a5bd67ba9fc0ec9bcbf597095ef91bbe2b67 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Sat, 7 Sep 2019 17:22:56 +0200
Subject: [PATCH 02/15] Added canAddEntries flag to InspectedElementTree

---
 .../src/devtools/views/Components/SelectedElement.js             | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js
index a553f494af1a9..ff848c7a943c2 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js
@@ -357,6 +357,7 @@ function InspectedElementView({
         inspectPath={inspectPropsPath}
         overrideValueFn={overridePropsFn}
         showWhenEmpty={true}
+        canAddEntries={true}
       />
       {type === ElementTypeSuspense ? (
         <InspectedElementTree

From e37c1faed3dacf681dc4ae06e4a34d8ab8c814c2 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Sat, 7 Sep 2019 17:24:50 +0200
Subject: [PATCH 03/15] Added EditableKey component.

---
 .../devtools/views/Components/EditableKey.css | 15 ++++
 .../devtools/views/Components/EditableKey.js  | 80 +++++++++++++++++++
 2 files changed, 95 insertions(+)
 create mode 100644 packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css
 create mode 100644 packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css
new file mode 100644
index 0000000000000..7bd2ca428aa43
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css
@@ -0,0 +1,15 @@
+.Input {
+  width: 100px;
+  background: none;
+  border: 1px solid transparent;
+  color: var(--color-attribute-name);
+  border-radius: 0.125rem;
+  font-family: var(--font-family-monospace);
+  font-size: var(--font-size-monospace-normal);
+}
+
+.Input:focus {
+  color: var(--color-attribute-editable-value);
+  background-color: var(--color-button-background-focus);
+  outline: none;
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js
new file mode 100644
index 0000000000000..a9cffd2c4001c
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js
@@ -0,0 +1,80 @@
+/**
+ * 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 React, {Fragment, useRef, useCallback, useEffect, useState} from 'react';
+import styles from './EditableKey.css';
+
+type OverrideKeyFn = (path: Array<string | number>, value: any) => void;
+
+type EditableKeyProps = {|
+  key?: string,
+  overrideKeyFn: OverrideKeyFn,
+|};
+
+export default function EditableKey({
+  key = '',
+  overrideKeyFn,
+}: EditableKeyProps) {
+  const [editableKey, setEditableKey] = useState(key);
+  const [isValid, setIsValid] = useState(false);
+  const inputRef = useRef<HTMLInputElement | null>(null);
+
+  useEffect(
+    () => {
+      if (inputRef.current !== null) {
+        inputRef.current.focus();
+      }
+    },
+    [inputRef],
+  );
+
+  const handleChange = useCallback(
+    ({target}) => {
+      const value = target.value.trim();
+
+      if (value) {
+        setIsValid(true);
+      } else {
+        setIsValid(false);
+      }
+
+      setEditableKey(value);
+    },
+    [overrideKeyFn],
+  );
+
+  const handleKeyDown = useCallback(
+    event => {
+      // Prevent keydown events from e.g. change selected element in the tree
+      event.stopPropagation();
+
+      const eventKey = event.key;
+
+      if ((eventKey === 'Enter' || eventKey === 'Tab') && isValid) {
+        overrideKeyFn(editableKey);
+      } else if (eventKey === 'Escape') {
+        setEditableKey(key);
+      }
+    },
+    [editableKey, setEditableKey, isValid, key, overrideKeyFn],
+  );
+
+  return (
+    <Fragment>
+      <input
+        className={styles.Input}
+        onChange={handleChange}
+        onKeyDown={handleKeyDown}
+        ref={inputRef}
+        type="text"
+        value={editableKey}
+      />
+    </Fragment>
+  );
+}

From 33611461a2e2f421545abf9ec4f239177c142731 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Sat, 7 Sep 2019 17:25:53 +0200
Subject: [PATCH 04/15] Added support to add an additional entry.

---
 .../views/Components/InspectedElementTree.css |  5 ++
 .../views/Components/InspectedElementTree.js  | 67 +++++++++++++++++--
 2 files changed, 67 insertions(+), 5 deletions(-)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css
index 620fbf412dd04..4bb01abfb76ad 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css
@@ -46,3 +46,8 @@
   font-style: italic;
   padding-left: 0.75rem;
 }
+
+.AddEntry {
+  display: flex;
+  padding-left: 0.9rem;
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
index 1acc27af5a82c..912d0eb84b407 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
@@ -8,10 +8,12 @@
  */
 
 import {copy} from 'clipboard-js';
-import React, {useCallback} from 'react';
+import React, {useEffect, useCallback, useState} from 'react';
 import Button from '../Button';
 import ButtonIcon from '../ButtonIcon';
 import KeyValue from './KeyValue';
+import EditableKey from './EditableKey';
+import EditableValue from './EditableValue';
 import {alphaSortEntries, serializeDataForCopy} from '../utils';
 import styles from './InspectedElementTree.css';
 
@@ -25,6 +27,7 @@ type Props = {|
   label: string,
   overrideValueFn?: ?OverrideValueFn,
   showWhenEmpty?: boolean,
+  canAddEntries?: boolean,
 |};
 
 export default function InspectedElementTree({
@@ -32,12 +35,22 @@ export default function InspectedElementTree({
   inspectPath,
   label,
   overrideValueFn,
+  canAddEntries = false,
   showWhenEmpty = false,
 }: Props) {
-  const entries = data != null ? Object.entries(data) : null;
-  if (entries !== null) {
-    entries.sort(alphaSortEntries);
-  }
+  const [entries, setEntries] = useState(null);
+  const [entryToAdd, setEntryToAdd] = useState(null);
+
+  useEffect(
+    () => {
+      if (data != null) {
+        setEntries(Object.entries(data).sort(alphaSortEntries));
+      } else {
+        setEntries(null);
+      }
+    },
+    [data],
+  );
 
   const isEmpty = entries === null || entries.length === 0;
 
@@ -46,6 +59,34 @@ export default function InspectedElementTree({
     [data],
   );
 
+  const handleEntryAdd = useCallback(
+    () => {
+      setEntryToAdd({
+        key: null,
+        value: '',
+      });
+    },
+    [setEntryToAdd],
+  );
+
+  const handleEntryAddName = useCallback(
+    key => {
+      setEntryToAdd({
+        ...entryToAdd,
+        key,
+      });
+    },
+    [entryToAdd, setEntryToAdd],
+  );
+
+  const handleEntryAddValue = useCallback(
+    (...args) => {
+      setEntryToAdd(null);
+      overrideValueFn(...args);
+    },
+    [overrideValueFn, setEntryToAdd],
+  );
+
   if (isEmpty && !showWhenEmpty) {
     return null;
   } else {
@@ -58,6 +99,11 @@ export default function InspectedElementTree({
               <ButtonIcon type="copy" />
             </Button>
           )}
+          {canAddEntries && (
+            <Button onClick={handleEntryAdd} title={`Add ${label}`}>
+              <ButtonIcon type="add" />
+            </Button>
+          )}
         </div>
         {isEmpty && <div className={styles.Empty}>None</div>}
         {!isEmpty &&
@@ -73,6 +119,17 @@ export default function InspectedElementTree({
               value={value}
             />
           ))}
+        {entryToAdd && (
+          <div className={styles.AddEntry}>
+            <EditableKey overrideKeyFn={handleEntryAddName} />:
+            <EditableValue
+              dataType={typeof entryToAdd.value}
+              overrideValueFn={handleEntryAddValue}
+              path={[entryToAdd.key]}
+              value={entryToAdd.value}
+            />
+          </div>
+        )}
       </div>
     );
   }

From 9661a48992767b0920f31ceffa6afba954738c1f Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Sat, 7 Sep 2019 17:27:30 +0200
Subject: [PATCH 05/15] Added support to add more complex data structures in
 the EditableValue component. Added support to change the dataType of the
 value that is being changed.

---
 .../views/Components/EditableValue.css        | 18 ++++-
 .../views/Components/EditableValue.js         | 78 ++++++++++---------
 2 files changed, 60 insertions(+), 36 deletions(-)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.css b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.css
index 3087b852617e0..30fa8a43ee657 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.css
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.css
@@ -19,7 +19,23 @@
   font-family: var(--font-family-monospace);
   font-size: var(--font-size-monospace-normal);
 }
-.Input:focus {
+
+.Invalid {
+  flex: 1 1;
+  background: none;
+  border: 1px solid transparent;
+  color: var(--color-attribute-editable-value);
+  border-radius: 0.125rem;
+  font-family: var(--font-family-monospace);
+  font-size: var(--font-size-monospace-normal);
+  background-color: var(--color-background-invalid);
+  color: var(--color-text-invalid);
+
+  --color-border: var(--color-text-invalid);
+}
+
+.Input:focus,
+.Invalid:focus {
   background-color: var(--color-button-background-focus);
   outline: none;
 }
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
index 46bc177763fe0..412edc4820722 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
@@ -7,10 +7,11 @@
  * @flow
  */
 
-import React, {Fragment, useCallback, useRef, useState} from 'react';
+import React, {Fragment, useEffect, useCallback, useRef, useState} from 'react';
 import Button from '../Button';
 import ButtonIcon from '../ButtonIcon';
 import styles from './EditableValue.css';
+import {sanitizeForParse} from '../../utils';
 
 type OverrideValueFn = (path: Array<string | number>, value: any) => void;
 
@@ -27,20 +28,38 @@ export default function EditableValue({
   path,
   value,
 }: EditableValueProps) {
+  const [isValid, setIsValid] = useState(true);
   const [hasPendingChanges, setHasPendingChanges] = useState(false);
-  const [editableValue, setEditableValue] = useState(value);
+  const [stringifiedValue, setStringifiedValue] = useState(
+    JSON.stringify(value),
+  );
+  const [editableValue, setEditableValue] = useState(stringifiedValue);
   const inputRef = useRef<HTMLInputElement | null>(null);
 
-  if (hasPendingChanges && editableValue === value) {
+  useEffect(
+    () => {
+      setStringifiedValue(JSON.stringify(value));
+    },
+    [value],
+  );
+
+  if (hasPendingChanges && editableValue === stringifiedValue) {
     setHasPendingChanges(false);
   }
 
   const handleChange = useCallback(
     ({target}) => {
       if (dataType === 'boolean') {
-        setEditableValue(target.checked);
+        setEditableValue(JSON.stringify(target.checked));
         overrideValueFn(path, target.checked);
       } else {
+        let isValidJSON = false;
+        try {
+          JSON.parse(sanitizeForParse(target.value));
+          isValidJSON = true;
+        } catch (error) {}
+
+        setIsValid(isValidJSON);
         setEditableValue(target.value);
       }
       setHasPendingChanges(true);
@@ -50,14 +69,15 @@ export default function EditableValue({
 
   const handleReset = useCallback(
     () => {
-      setEditableValue(value);
+      setEditableValue(stringifiedValue);
       setHasPendingChanges(false);
+      setIsValid(true);
 
       if (inputRef.current !== null) {
         inputRef.current.focus();
       }
     },
-    [value],
+    [stringifiedValue],
   );
 
   const handleKeyDown = useCallback(
@@ -67,47 +87,35 @@ export default function EditableValue({
 
       const {key} = event;
 
-      if (key === 'Enter') {
-        if (dataType === 'number') {
-          const parsedValue = parseFloat(editableValue);
-          if (!Number.isNaN(parsedValue)) {
-            overrideValueFn(path, parsedValue);
-          }
-        } else {
-          overrideValueFn(path, editableValue);
+      if (key === 'Enter' && isValid) {
+        const parsedEditableValue = JSON.parse(sanitizeForParse(editableValue));
+
+        if (value !== parsedEditableValue) {
+          overrideValueFn(path, parsedEditableValue);
         }
 
         // Don't reset the pending change flag here.
         // The inspected fiber won't be updated until after the next "inspectElement" message.
         // We'll reset that flag during a subsequent render.
       } else if (key === 'Escape') {
-        setEditableValue(value);
+        setEditableValue(stringifiedValue);
         setHasPendingChanges(false);
+        setIsValid(true);
       }
     },
-    [editableValue, dataType, overrideValueFn, path, value],
+    [editableValue, isValid, dataType, overrideValueFn, path, value],
   );
 
-  // Render different input types based on the dataType
-  let type = 'text';
-  if (dataType === 'boolean') {
-    type = 'checkbox';
-  } else if (dataType === 'number') {
-    type = 'number';
-  }
-
-  let inputValue = value == null ? '' : value;
+  let inputValue = value === undefined ? '' : stringifiedValue;
   if (hasPendingChanges) {
-    inputValue = editableValue == null ? '' : editableValue;
+    inputValue = editableValue;
   }
 
   let placeholder = '';
-  if (value === null) {
-    placeholder = '(null)';
-  } else if (value === undefined) {
+  if (value === undefined) {
     placeholder = '(undefined)';
-  } else if (dataType === 'string') {
-    placeholder = '(string)';
+  } else {
+    placeholder = 'Enter valid JSON';
   }
 
   return (
@@ -115,23 +123,23 @@ export default function EditableValue({
       {dataType === 'boolean' && (
         <label className={styles.CheckboxLabel}>
           <input
-            checked={inputValue}
+            checked={inputValue === 'true'}
             className={styles.Checkbox}
             onChange={handleChange}
             onKeyDown={handleKeyDown}
             ref={inputRef}
-            type={type}
+            type="checkbox"
           />
         </label>
       )}
       {dataType !== 'boolean' && (
         <input
-          className={styles.Input}
+          className={isValid ? styles.Input : styles.Invalid}
           onChange={handleChange}
           onKeyDown={handleKeyDown}
           placeholder={placeholder}
           ref={inputRef}
-          type={type}
+          type="text"
           value={inputValue}
         />
       )}

From c61c8f7880c3acdff5528cf88f11a3ff179d88b6 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Sat, 7 Sep 2019 17:59:16 +0200
Subject: [PATCH 06/15] Fixed flow error.

---
 .../src/devtools/views/Components/InspectedElementTree.js    | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
index 912d0eb84b407..41a13499fa1af 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
@@ -82,7 +82,10 @@ export default function InspectedElementTree({
   const handleEntryAddValue = useCallback(
     (...args) => {
       setEntryToAdd(null);
-      overrideValueFn(...args);
+
+      if (typeof overrideValueFn === 'function') {
+        overrideValueFn(...args);
+      }
     },
     [overrideValueFn, setEntryToAdd],
   );

From 36c4905f031844de468faae06f307e0658dbf163 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Sat, 7 Sep 2019 19:36:27 +0200
Subject: [PATCH 07/15] Removed unneeded fragment.

---
 .../devtools/views/Components/EditableKey.js  | 20 +++++++++----------
 1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js
index a9cffd2c4001c..e1d10a244833c 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js
@@ -7,7 +7,7 @@
  * @flow
  */
 
-import React, {Fragment, useRef, useCallback, useEffect, useState} from 'react';
+import React, {useRef, useCallback, useEffect, useState} from 'react';
 import styles from './EditableKey.css';
 
 type OverrideKeyFn = (path: Array<string | number>, value: any) => void;
@@ -66,15 +66,13 @@ export default function EditableKey({
   );
 
   return (
-    <Fragment>
-      <input
-        className={styles.Input}
-        onChange={handleChange}
-        onKeyDown={handleKeyDown}
-        ref={inputRef}
-        type="text"
-        value={editableKey}
-      />
-    </Fragment>
+    <input
+      className={styles.Input}
+      onChange={handleChange}
+      onKeyDown={handleKeyDown}
+      ref={inputRef}
+      type="text"
+      value={editableKey}
+    />
   );
 }

From a30cfa4bd1f66f9319a26ac74c63054406b7aa44 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Tue, 10 Sep 2019 09:11:22 +0200
Subject: [PATCH 08/15] Renamed EditableKey -> EditableName

---
 .../{EditableKey.css => EditableName.css}     |  0
 .../{EditableKey.js => EditableName.js}       | 30 +++++++++----------
 .../views/Components/InspectedElementTree.js  |  4 +--
 .../src/app/EditableProps/index.js            | 11 ++++++-
 4 files changed, 27 insertions(+), 18 deletions(-)
 rename packages/react-devtools-shared/src/devtools/views/Components/{EditableKey.css => EditableName.css} (100%)
 rename packages/react-devtools-shared/src/devtools/views/Components/{EditableKey.js => EditableName.js} (69%)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.css
similarity index 100%
rename from packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css
rename to packages/react-devtools-shared/src/devtools/views/Components/EditableName.css
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
similarity index 69%
rename from packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js
rename to packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
index e1d10a244833c..3d2e6cb332dbb 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
@@ -8,20 +8,20 @@
  */
 
 import React, {useRef, useCallback, useEffect, useState} from 'react';
-import styles from './EditableKey.css';
+import styles from './EditableName.css';
 
-type OverrideKeyFn = (path: Array<string | number>, value: any) => void;
+type OverrideNameFn = (path: Array<string | number>, value: any) => void;
 
-type EditableKeyProps = {|
+type EditableNameProps = {|
   key?: string,
-  overrideKeyFn: OverrideKeyFn,
+  overrideNameFn: OverrideNameFn,
 |};
 
-export default function EditableKey({
-  key = '',
-  overrideKeyFn,
-}: EditableKeyProps) {
-  const [editableKey, setEditableKey] = useState(key);
+export default function EditableName({
+  name = '',
+  overrideNameFn,
+}: EditableNameProps) {
+  const [editableName, setEditableName] = useState(name);
   const [isValid, setIsValid] = useState(false);
   const inputRef = useRef<HTMLInputElement | null>(null);
 
@@ -44,9 +44,9 @@ export default function EditableKey({
         setIsValid(false);
       }
 
-      setEditableKey(value);
+      setEditableName(value);
     },
-    [overrideKeyFn],
+    [overrideNameFn],
   );
 
   const handleKeyDown = useCallback(
@@ -57,12 +57,12 @@ export default function EditableKey({
       const eventKey = event.key;
 
       if ((eventKey === 'Enter' || eventKey === 'Tab') && isValid) {
-        overrideKeyFn(editableKey);
+        overrideNameFn(editableName);
       } else if (eventKey === 'Escape') {
-        setEditableKey(key);
+        setEditableName(name);
       }
     },
-    [editableKey, setEditableKey, isValid, key, overrideKeyFn],
+    [editableName, setEditableName, isValid, name, overrideNameFn],
   );
 
   return (
@@ -72,7 +72,7 @@ export default function EditableKey({
       onKeyDown={handleKeyDown}
       ref={inputRef}
       type="text"
-      value={editableKey}
+      value={editableName}
     />
   );
 }
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
index 41a13499fa1af..55e0702125ee4 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
@@ -12,7 +12,7 @@ import React, {useEffect, useCallback, useState} from 'react';
 import Button from '../Button';
 import ButtonIcon from '../ButtonIcon';
 import KeyValue from './KeyValue';
-import EditableKey from './EditableKey';
+import EditableName from './EditableName';
 import EditableValue from './EditableValue';
 import {alphaSortEntries, serializeDataForCopy} from '../utils';
 import styles from './InspectedElementTree.css';
@@ -124,7 +124,7 @@ export default function InspectedElementTree({
           ))}
         {entryToAdd && (
           <div className={styles.AddEntry}>
-            <EditableKey overrideKeyFn={handleEntryAddName} />:
+            <EditableName overrideNameFn={handleEntryAddName} />:
             <EditableValue
               dataType={typeof entryToAdd.value}
               overrideValueFn={handleEntryAddValue}
diff --git a/packages/react-devtools-shell/src/app/EditableProps/index.js b/packages/react-devtools-shell/src/app/EditableProps/index.js
index 220a58c17be32..9efa1cd5f5070 100644
--- a/packages/react-devtools-shell/src/app/EditableProps/index.js
+++ b/packages/react-devtools-shell/src/app/EditableProps/index.js
@@ -126,7 +126,16 @@ export default function EditableProps() {
     <Fragment>
       <h1>Editable props</h1>
       <strong>Class</strong>
-      <StatefulClass name="Brian" toggle={true} />
+      <StatefulClass
+        name="Brian"
+        toggle={true}
+        newLineStringProp={'Hello \n World'}
+        nanProp={NaN}
+        infinityProp={Infinity}
+        minusInfinityProp={-Infinity}
+        emptyStringProp={''}
+        nullProp={null}
+      />
       <strong>Function</strong>
       <StatefulFunction name="Brian" />
       <strong>Memoized Class</strong>

From 578947267fde93118e709f7d4cbd843e80b7957a Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Tue, 10 Sep 2019 09:12:11 +0200
Subject: [PATCH 09/15] Removed unneeded dependency

---
 .../src/devtools/views/Components/EditableName.js               | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
index 3d2e6cb332dbb..d18c5694df0bf 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
@@ -31,7 +31,7 @@ export default function EditableName({
         inputRef.current.focus();
       }
     },
-    [inputRef],
+    [],
   );
 
   const handleChange = useCallback(

From 20280fbe3ff64c36ca6c91c1eda08fb299350868 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Tue, 10 Sep 2019 09:59:28 +0200
Subject: [PATCH 10/15] Removed problematic props to state hook.

---
 .../devtools/views/Components/EditableName.js |  6 +--
 .../views/Components/EditableValue.js         | 40 +++++++++----------
 .../views/Components/InspectedElementTree.js  |  2 +-
 .../src/devtools/views/Components/KeyValue.js |  3 +-
 4 files changed, 24 insertions(+), 27 deletions(-)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
index d18c5694df0bf..adfea2640629e 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
@@ -13,15 +13,15 @@ import styles from './EditableName.css';
 type OverrideNameFn = (path: Array<string | number>, value: any) => void;
 
 type EditableNameProps = {|
-  key?: string,
+  initialValue?: string,
   overrideNameFn: OverrideNameFn,
 |};
 
 export default function EditableName({
-  name = '',
+  initialValue = '',
   overrideNameFn,
 }: EditableNameProps) {
-  const [editableName, setEditableName] = useState(name);
+  const [editableName, setEditableName] = useState(initialValue);
   const [isValid, setIsValid] = useState(false);
   const inputRef = useRef<HTMLInputElement | null>(null);
 
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
index 412edc4820722..a6b2e6abe7d85 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
@@ -19,38 +19,34 @@ type EditableValueProps = {|
   dataType: string,
   overrideValueFn: OverrideValueFn,
   path: Array<string | number>,
-  value: any,
+  initialValue: any,
 |};
 
+function useEditableValue(initialValue) {
+  const [editableValue, setEditableValue] = useState(JSON.stringify(initialValue));
+
+  return [editableValue, (value, {shouldStringify} = {}) => (shouldStringify ? setEditableValue(JSON.stringify(value)) : setEditableValue(value))];
+}
+
 export default function EditableValue({
   dataType,
   overrideValueFn,
   path,
-  value,
+  initialValue,
 }: EditableValueProps) {
   const [isValid, setIsValid] = useState(true);
   const [hasPendingChanges, setHasPendingChanges] = useState(false);
-  const [stringifiedValue, setStringifiedValue] = useState(
-    JSON.stringify(value),
-  );
-  const [editableValue, setEditableValue] = useState(stringifiedValue);
+  const [editableValue, setEditableValue] = useEditableValue(initialValue);
   const inputRef = useRef<HTMLInputElement | null>(null);
 
-  useEffect(
-    () => {
-      setStringifiedValue(JSON.stringify(value));
-    },
-    [value],
-  );
-
-  if (hasPendingChanges && editableValue === stringifiedValue) {
+  if (hasPendingChanges && editableValue === JSON.stringify(initialValue)) {
     setHasPendingChanges(false);
   }
 
   const handleChange = useCallback(
     ({target}) => {
       if (dataType === 'boolean') {
-        setEditableValue(JSON.stringify(target.checked));
+        setEditableValue(target.checked, {shouldStringify: true});
         overrideValueFn(path, target.checked);
       } else {
         let isValidJSON = false;
@@ -69,7 +65,7 @@ export default function EditableValue({
 
   const handleReset = useCallback(
     () => {
-      setEditableValue(stringifiedValue);
+      setEditableValue(initialValue, {shouldStringify: true});
       setHasPendingChanges(false);
       setIsValid(true);
 
@@ -77,7 +73,7 @@ export default function EditableValue({
         inputRef.current.focus();
       }
     },
-    [stringifiedValue],
+    [initialValue],
   );
 
   const handleKeyDown = useCallback(
@@ -90,7 +86,7 @@ export default function EditableValue({
       if (key === 'Enter' && isValid) {
         const parsedEditableValue = JSON.parse(sanitizeForParse(editableValue));
 
-        if (value !== parsedEditableValue) {
+        if (initialValue !== parsedEditableValue) {
           overrideValueFn(path, parsedEditableValue);
         }
 
@@ -98,21 +94,21 @@ export default function EditableValue({
         // The inspected fiber won't be updated until after the next "inspectElement" message.
         // We'll reset that flag during a subsequent render.
       } else if (key === 'Escape') {
-        setEditableValue(stringifiedValue);
+        setEditableValue(initialValue, {shouldStringify: true});
         setHasPendingChanges(false);
         setIsValid(true);
       }
     },
-    [editableValue, isValid, dataType, overrideValueFn, path, value],
+    [editableValue, isValid, dataType, overrideValueFn, path, initialValue],
   );
 
-  let inputValue = value === undefined ? '' : stringifiedValue;
+  let inputValue = initialValue === undefined ? '' : JSON.stringify(initialValue);
   if (hasPendingChanges) {
     inputValue = editableValue;
   }
 
   let placeholder = '';
-  if (value === undefined) {
+  if (initialValue === undefined) {
     placeholder = '(undefined)';
   } else {
     placeholder = 'Enter valid JSON';
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
index 55e0702125ee4..54f9aec97fb1e 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
@@ -129,7 +129,7 @@ export default function InspectedElementTree({
               dataType={typeof entryToAdd.value}
               overrideValueFn={handleEntryAddValue}
               path={[entryToAdd.key]}
-              value={entryToAdd.value}
+              initialValue={entryToAdd.value}
             />
           </div>
         )}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
index 8844a13f6807d..5480916384421 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
@@ -16,6 +16,7 @@ import {meta} from '../../../hydration';
 import styles from './KeyValue.css';
 
 import type {InspectPath} from './SelectedElement';
+import EditableName from "./EditableName";
 
 type OverrideValueFn = (path: Array<string | number>, value: any) => void;
 
@@ -105,7 +106,7 @@ export default function KeyValue({
             dataType={dataType}
             overrideValueFn={((overrideValueFn: any): OverrideValueFn)}
             path={path}
-            value={value}
+            initialValue={value}
           />
         ) : (
           <span className={styles.Value}>{displayValue}</span>

From c53c79a0158fe2ed77daf8e589e37f5046673971 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Tue, 10 Sep 2019 10:09:55 +0200
Subject: [PATCH 11/15] Prettified changes.

---
 .../devtools/views/Components/EditableName.js | 17 +++++------
 .../views/Components/EditableValue.js         | 30 ++++++++++++++-----
 .../src/devtools/views/Components/KeyValue.js |  2 +-
 3 files changed, 30 insertions(+), 19 deletions(-)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
index adfea2640629e..d96653c603d61 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
@@ -25,14 +25,11 @@ export default function EditableName({
   const [isValid, setIsValid] = useState(false);
   const inputRef = useRef<HTMLInputElement | null>(null);
 
-  useEffect(
-    () => {
-      if (inputRef.current !== null) {
-        inputRef.current.focus();
-      }
-    },
-    [],
-  );
+  useEffect(() => {
+    if (inputRef.current !== null) {
+      inputRef.current.focus();
+    }
+  }, []);
 
   const handleChange = useCallback(
     ({target}) => {
@@ -59,10 +56,10 @@ export default function EditableName({
       if ((eventKey === 'Enter' || eventKey === 'Tab') && isValid) {
         overrideNameFn(editableName);
       } else if (eventKey === 'Escape') {
-        setEditableName(name);
+        setEditableName(initialValue);
       }
     },
-    [editableName, setEditableName, isValid, name, overrideNameFn],
+    [editableName, setEditableName, isValid, initialValue, overrideNameFn],
   );
 
   return (
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
index a6b2e6abe7d85..58f3d8c8f6470 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
@@ -7,7 +7,7 @@
  * @flow
  */
 
-import React, {Fragment, useEffect, useCallback, useRef, useState} from 'react';
+import React, {Fragment, useCallback, useRef, useState} from 'react';
 import Button from '../Button';
 import ButtonIcon from '../ButtonIcon';
 import styles from './EditableValue.css';
@@ -22,12 +22,6 @@ type EditableValueProps = {|
   initialValue: any,
 |};
 
-function useEditableValue(initialValue) {
-  const [editableValue, setEditableValue] = useState(JSON.stringify(initialValue));
-
-  return [editableValue, (value, {shouldStringify} = {}) => (shouldStringify ? setEditableValue(JSON.stringify(value)) : setEditableValue(value))];
-}
-
 export default function EditableValue({
   dataType,
   overrideValueFn,
@@ -102,7 +96,8 @@ export default function EditableValue({
     [editableValue, isValid, dataType, overrideValueFn, path, initialValue],
   );
 
-  let inputValue = initialValue === undefined ? '' : JSON.stringify(initialValue);
+  let inputValue =
+    initialValue === undefined ? '' : JSON.stringify(initialValue);
   if (hasPendingChanges) {
     inputValue = editableValue;
   }
@@ -151,3 +146,22 @@ export default function EditableValue({
     </Fragment>
   );
 }
+
+function useEditableValue(initialValue: any): [any, Function] {
+  const [editableValue, setEditableValue] = useState(
+    JSON.stringify(initialValue),
+  );
+
+  function setEditableValueWithStringify(
+    value: any,
+    {shouldStringify}: Object = {},
+  ) {
+    if (shouldStringify) {
+      setEditableValue(JSON.stringify(value));
+    }
+
+    setEditableValue(value);
+  }
+
+  return [editableValue, setEditableValueWithStringify];
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
index 5480916384421..662a5f0e91a3a 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
@@ -16,7 +16,7 @@ import {meta} from '../../../hydration';
 import styles from './KeyValue.css';
 
 import type {InspectPath} from './SelectedElement';
-import EditableName from "./EditableName";
+import EditableName from './EditableName';
 
 type OverrideValueFn = (path: Array<string | number>, value: any) => void;
 

From a5cc62b2c5b849b51732508876ed38554ef15b90 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Tue, 10 Sep 2019 10:16:16 +0200
Subject: [PATCH 12/15] Removed unused import.

---
 .../src/devtools/views/Components/KeyValue.js                    | 1 -
 1 file changed, 1 deletion(-)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
index 662a5f0e91a3a..e05a35fc57a7f 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
@@ -16,7 +16,6 @@ import {meta} from '../../../hydration';
 import styles from './KeyValue.css';
 
 import type {InspectPath} from './SelectedElement';
-import EditableName from './EditableName';
 
 type OverrideValueFn = (path: Array<string | number>, value: any) => void;
 

From d4c415adb4e73f026dfc84e07fb3078d6598cd82 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Tue, 10 Sep 2019 11:35:18 +0200
Subject: [PATCH 13/15] Fixed shouldStringify check.

---
 .../src/devtools/views/Components/EditableValue.js            | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
index 58f3d8c8f6470..fbe98fb348047 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
@@ -158,9 +158,9 @@ function useEditableValue(initialValue: any): [any, Function] {
   ) {
     if (shouldStringify) {
       setEditableValue(JSON.stringify(value));
+    } else {
+      setEditableValue(value);
     }
-
-    setEditableValue(value);
   }
 
   return [editableValue, setEditableValueWithStringify];

From 389010a27e9a0bb0f332dff844ff744b23e1a5e0 Mon Sep 17 00:00:00 2001
From: Hristo Kanchev <hristokkanchev@gmail.com>
Date: Tue, 10 Sep 2019 11:56:35 +0200
Subject: [PATCH 14/15] Removed testing props from EditableProps.

---
 .../src/app/EditableProps/index.js                    | 11 +----------
 1 file changed, 1 insertion(+), 10 deletions(-)

diff --git a/packages/react-devtools-shell/src/app/EditableProps/index.js b/packages/react-devtools-shell/src/app/EditableProps/index.js
index 9efa1cd5f5070..220a58c17be32 100644
--- a/packages/react-devtools-shell/src/app/EditableProps/index.js
+++ b/packages/react-devtools-shell/src/app/EditableProps/index.js
@@ -126,16 +126,7 @@ export default function EditableProps() {
     <Fragment>
       <h1>Editable props</h1>
       <strong>Class</strong>
-      <StatefulClass
-        name="Brian"
-        toggle={true}
-        newLineStringProp={'Hello \n World'}
-        nanProp={NaN}
-        infinityProp={Infinity}
-        minusInfinityProp={-Infinity}
-        emptyStringProp={''}
-        nullProp={null}
-      />
+      <StatefulClass name="Brian" toggle={true} />
       <strong>Function</strong>
       <StatefulFunction name="Brian" />
       <strong>Memoized Class</strong>

From 0373472c988b87ba63af25556b770453a31084bd Mon Sep 17 00:00:00 2001
From: Brian Vaughn <bvaughn@fb.com>
Date: Tue, 10 Sep 2019 13:19:09 -0700
Subject: [PATCH 15/15] Made some inline tweaks

---
 .../src/devtools/utils.js                     |  34 +++-
 .../views/Components/EditableName.css         |  18 +-
 .../devtools/views/Components/EditableName.js |  35 ++--
 .../views/Components/EditableValue.js         | 164 +++++-------------
 .../views/Components/InspectedElementTree.css |   4 +-
 .../views/Components/InspectedElementTree.js  |  77 +++-----
 .../src/devtools/views/hooks.js               |  66 ++++++-
 7 files changed, 198 insertions(+), 200 deletions(-)

diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js
index 6cd23d4dbf3ce..0eb75451f2b6c 100644
--- a/packages/react-devtools-shared/src/devtools/utils.js
+++ b/packages/react-devtools-shared/src/devtools/utils.js
@@ -88,10 +88,40 @@ export function printStore(store: Store, includeWeight: boolean = false) {
 // so this method replaces e.g. 'foo' with "foo"
 export function sanitizeForParse(value: any) {
   if (typeof value === 'string') {
-    if (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'") {
+    if (
+      value.length >= 2 &&
+      value.charAt(0) === "'" &&
+      value.charAt(value.length - 1) === "'"
+    ) {
       return '"' + value.substr(1, value.length - 2) + '"';
     }
   }
-
   return value;
 }
+
+export function smartParse(value: any) {
+  switch (value) {
+    case 'Infinity':
+      return Infinity;
+    case 'NaN':
+      return NaN;
+    case 'undefined':
+      return undefined;
+    default:
+      return JSON.parse(sanitizeForParse(value));
+  }
+}
+
+export function smartStringify(value: any) {
+  if (typeof value === 'number') {
+    if (Number.isNaN(value)) {
+      return 'NaN';
+    } else if (!Number.isFinite(value)) {
+      return 'Infinity';
+    }
+  } else if (value === undefined) {
+    return 'undefined';
+  }
+
+  return JSON.stringify(value);
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.css b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.css
index 7bd2ca428aa43..38b1f1d92b4f8 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.css
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.css
@@ -1,15 +1,9 @@
 .Input {
-  width: 100px;
-  background: none;
-  border: 1px solid transparent;
-  color: var(--color-attribute-name);
-  border-radius: 0.125rem;
-  font-family: var(--font-family-monospace);
-  font-size: var(--font-size-monospace-normal);
+  flex: 0 1 auto;
+  padding: 1px;
+  box-shadow: 0px 1px 3px transparent;
 }
-
 .Input:focus {
-  color: var(--color-attribute-editable-value);
-  background-color: var(--color-button-background-focus);
-  outline: none;
-}
+  color: var(--color-text);
+  box-shadow: 0px 1px 3px var(--color-shadow);
+}
\ No newline at end of file
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
index d96653c603d61..73b85cddf8c42 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
@@ -7,29 +7,25 @@
  * @flow
  */
 
-import React, {useRef, useCallback, useEffect, useState} from 'react';
+import React, {useCallback, useState} from 'react';
+import AutoSizeInput from './NativeStyleEditor/AutoSizeInput';
 import styles from './EditableName.css';
 
 type OverrideNameFn = (path: Array<string | number>, value: any) => void;
 
 type EditableNameProps = {|
+  autoFocus?: boolean,
   initialValue?: string,
   overrideNameFn: OverrideNameFn,
 |};
 
 export default function EditableName({
+  autoFocus = false,
   initialValue = '',
   overrideNameFn,
 }: EditableNameProps) {
   const [editableName, setEditableName] = useState(initialValue);
   const [isValid, setIsValid] = useState(false);
-  const inputRef = useRef<HTMLInputElement | null>(null);
-
-  useEffect(() => {
-    if (inputRef.current !== null) {
-      inputRef.current.focus();
-    }
-  }, []);
 
   const handleChange = useCallback(
     ({target}) => {
@@ -51,23 +47,30 @@ export default function EditableName({
       // Prevent keydown events from e.g. change selected element in the tree
       event.stopPropagation();
 
-      const eventKey = event.key;
-
-      if ((eventKey === 'Enter' || eventKey === 'Tab') && isValid) {
-        overrideNameFn(editableName);
-      } else if (eventKey === 'Escape') {
-        setEditableName(initialValue);
+      switch (event.key) {
+        case 'Enter':
+        case 'Tab':
+          if (isValid) {
+            overrideNameFn(editableName);
+          }
+          break;
+        case 'Escape':
+          setEditableName(initialValue);
+          break;
+        default:
+          break;
       }
     },
     [editableName, setEditableName, isValid, initialValue, overrideNameFn],
   );
 
   return (
-    <input
+    <AutoSizeInput
+      autoFocus={autoFocus}
       className={styles.Input}
       onChange={handleChange}
       onKeyDown={handleKeyDown}
-      ref={inputRef}
+      placeholder="new prop"
       type="text"
       value={editableName}
     />
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
index fbe98fb348047..96f505c20f188 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
@@ -7,103 +7,64 @@
  * @flow
  */
 
-import React, {Fragment, useCallback, useRef, useState} from 'react';
+import React, {Fragment, useCallback, useRef} from 'react';
 import Button from '../Button';
 import ButtonIcon from '../ButtonIcon';
 import styles from './EditableValue.css';
-import {sanitizeForParse} from '../../utils';
+import {useEditableValue} from '../hooks';
 
 type OverrideValueFn = (path: Array<string | number>, value: any) => void;
 
 type EditableValueProps = {|
   dataType: string,
+  initialValue: any,
   overrideValueFn: OverrideValueFn,
   path: Array<string | number>,
-  initialValue: any,
 |};
 
 export default function EditableValue({
   dataType,
+  initialValue,
   overrideValueFn,
   path,
-  initialValue,
 }: EditableValueProps) {
-  const [isValid, setIsValid] = useState(true);
-  const [hasPendingChanges, setHasPendingChanges] = useState(false);
-  const [editableValue, setEditableValue] = useEditableValue(initialValue);
   const inputRef = useRef<HTMLInputElement | null>(null);
-
-  if (hasPendingChanges && editableValue === JSON.stringify(initialValue)) {
-    setHasPendingChanges(false);
-  }
-
-  const handleChange = useCallback(
-    ({target}) => {
-      if (dataType === 'boolean') {
-        setEditableValue(target.checked, {shouldStringify: true});
-        overrideValueFn(path, target.checked);
-      } else {
-        let isValidJSON = false;
-        try {
-          JSON.parse(sanitizeForParse(target.value));
-          isValidJSON = true;
-        } catch (error) {}
-
-        setIsValid(isValidJSON);
-        setEditableValue(target.value);
-      }
-      setHasPendingChanges(true);
-    },
-    [dataType, overrideValueFn, path],
-  );
-
-  const handleReset = useCallback(
-    () => {
-      setEditableValue(initialValue, {shouldStringify: true});
-      setHasPendingChanges(false);
-      setIsValid(true);
-
-      if (inputRef.current !== null) {
-        inputRef.current.focus();
-      }
-    },
-    [initialValue],
-  );
+  const {
+    editableValue,
+    hasPendingChanges,
+    isValid,
+    parsedValue,
+    reset,
+    update,
+  } = useEditableValue(initialValue);
+
+  const handleChange = useCallback(({target}) => update(target.value), [
+    update,
+  ]);
 
   const handleKeyDown = useCallback(
     event => {
       // Prevent keydown events from e.g. change selected element in the tree
       event.stopPropagation();
 
-      const {key} = event;
-
-      if (key === 'Enter' && isValid) {
-        const parsedEditableValue = JSON.parse(sanitizeForParse(editableValue));
-
-        if (initialValue !== parsedEditableValue) {
-          overrideValueFn(path, parsedEditableValue);
-        }
-
-        // Don't reset the pending change flag here.
-        // The inspected fiber won't be updated until after the next "inspectElement" message.
-        // We'll reset that flag during a subsequent render.
-      } else if (key === 'Escape') {
-        setEditableValue(initialValue, {shouldStringify: true});
-        setHasPendingChanges(false);
-        setIsValid(true);
+      switch (event.key) {
+        case 'Enter':
+          if (isValid && hasPendingChanges) {
+            overrideValueFn(path, parsedValue);
+          }
+          break;
+        case 'Escape':
+          reset();
+          break;
+        default:
+          break;
       }
     },
-    [editableValue, isValid, dataType, overrideValueFn, path, initialValue],
+    [hasPendingChanges, isValid, overrideValueFn, parsedValue, reset],
   );
 
-  let inputValue =
-    initialValue === undefined ? '' : JSON.stringify(initialValue);
-  if (hasPendingChanges) {
-    inputValue = editableValue;
-  }
-
   let placeholder = '';
-  if (initialValue === undefined) {
+  if (editableValue === undefined) {
     placeholder = '(undefined)';
   } else {
     placeholder = 'Enter valid JSON';
@@ -111,57 +72,24 @@ export default function EditableValue({
 
   return (
     <Fragment>
-      {dataType === 'boolean' && (
-        <label className={styles.CheckboxLabel}>
-          <input
-            checked={inputValue === 'true'}
-            className={styles.Checkbox}
-            onChange={handleChange}
-            onKeyDown={handleKeyDown}
-            ref={inputRef}
-            type="checkbox"
-          />
-        </label>
+      <input
+        autoComplete="new-password"
+        className={isValid ? styles.Input : styles.Invalid}
+        onChange={handleChange}
+        onKeyDown={handleKeyDown}
+        placeholder={placeholder}
+        ref={inputRef}
+        type="text"
+        value={editableValue}
+      />
+      {hasPendingChanges && (
+        <Button
+          className={styles.ResetButton}
+          onClick={reset}
+          title="Reset value">
+          <ButtonIcon type="undo" />
+        </Button>
       )}
-      {dataType !== 'boolean' && (
-        <input
-          className={isValid ? styles.Input : styles.Invalid}
-          onChange={handleChange}
-          onKeyDown={handleKeyDown}
-          placeholder={placeholder}
-          ref={inputRef}
-          type="text"
-          value={inputValue}
-        />
-      )}
-      {hasPendingChanges &&
-        dataType !== 'boolean' && (
-          <Button
-            className={styles.ResetButton}
-            onClick={handleReset}
-            title="Reset value">
-            <ButtonIcon type="undo" />
-          </Button>
-        )}
     </Fragment>
   );
 }
-
-function useEditableValue(initialValue: any): [any, Function] {
-  const [editableValue, setEditableValue] = useState(
-    JSON.stringify(initialValue),
-  );
-
-  function setEditableValueWithStringify(
-    value: any,
-    {shouldStringify}: Object = {},
-  ) {
-    if (shouldStringify) {
-      setEditableValue(JSON.stringify(value));
-    } else {
-      setEditableValue(value);
-    }
-  }
-
-  return [editableValue, setEditableValueWithStringify];
-}
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css
index 4bb01abfb76ad..76c1e40761019 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css
@@ -48,6 +48,8 @@
 }
 
 .AddEntry {
+  padding-left: 1rem;
+  white-space: nowrap;
   display: flex;
-  padding-left: 0.9rem;
+  align-items: center;
 }
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
index 54f9aec97fb1e..edfb5f2c5679e 100644
--- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
+++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js
@@ -8,7 +8,7 @@
  */
 
 import {copy} from 'clipboard-js';
-import React, {useEffect, useCallback, useState} from 'react';
+import React, {useCallback, useState} from 'react';
 import Button from '../Button';
 import ButtonIcon from '../ButtonIcon';
 import KeyValue from './KeyValue';
@@ -38,19 +38,13 @@ export default function InspectedElementTree({
   canAddEntries = false,
   showWhenEmpty = false,
 }: Props) {
-  const [entries, setEntries] = useState(null);
-  const [entryToAdd, setEntryToAdd] = useState(null);
+  const entries = data != null ? Object.entries(data) : null;
+  if (entries !== null) {
+    entries.sort(alphaSortEntries);
+  }
 
-  useEffect(
-    () => {
-      if (data != null) {
-        setEntries(Object.entries(data).sort(alphaSortEntries));
-      } else {
-        setEntries(null);
-      }
-    },
-    [data],
-  );
+  const [newPropKey, setNewPropKey] = useState<number>(0);
+  const [newPropName, setNewPropName] = useState<string>('');
 
   const isEmpty = entries === null || entries.length === 0;
 
@@ -59,38 +53,23 @@ export default function InspectedElementTree({
     [data],
   );
 
-  const handleEntryAdd = useCallback(
-    () => {
-      setEntryToAdd({
-        key: null,
-        value: '',
-      });
-    },
-    [setEntryToAdd],
-  );
-
-  const handleEntryAddName = useCallback(
-    key => {
-      setEntryToAdd({
-        ...entryToAdd,
-        key,
-      });
-    },
-    [entryToAdd, setEntryToAdd],
-  );
+  const handleNewEntryValue = useCallback(
+    (name, value) => {
+      if (!newPropName) {
+        return;
+      }
 
-  const handleEntryAddValue = useCallback(
-    (...args) => {
-      setEntryToAdd(null);
+      setNewPropName('');
+      setNewPropKey(key => key + 1);
 
       if (typeof overrideValueFn === 'function') {
-        overrideValueFn(...args);
+        overrideValueFn(name, value);
       }
     },
-    [overrideValueFn, setEntryToAdd],
+    [newPropName, overrideValueFn],
   );
 
-  if (isEmpty && !showWhenEmpty) {
+  if (isEmpty && !showWhenEmpty && !canAddEntries) {
     return null;
   } else {
     return (
@@ -102,11 +81,6 @@ export default function InspectedElementTree({
               <ButtonIcon type="copy" />
             </Button>
           )}
-          {canAddEntries && (
-            <Button onClick={handleEntryAdd} title={`Add ${label}`}>
-              <ButtonIcon type="add" />
-            </Button>
-          )}
         </div>
         {isEmpty && <div className={styles.Empty}>None</div>}
         {!isEmpty &&
@@ -122,14 +96,17 @@ export default function InspectedElementTree({
               value={value}
             />
           ))}
-        {entryToAdd && (
-          <div className={styles.AddEntry}>
-            <EditableName overrideNameFn={handleEntryAddName} />:
+        {canAddEntries && (
+          <div className={styles.AddEntry} key={newPropKey}>
+            <EditableName
+              autoFocus={newPropKey > 0}
+              overrideNameFn={setNewPropName}
+            />
+            :&nbsp;
             <EditableValue
-              dataType={typeof entryToAdd.value}
-              overrideValueFn={handleEntryAddValue}
-              path={[entryToAdd.key]}
-              initialValue={entryToAdd.value}
+              initialValue={''}
+              overrideValueFn={handleNewEntryValue}
+              path={[newPropName]}
             />
           </div>
         )}
diff --git a/packages/react-devtools-shared/src/devtools/views/hooks.js b/packages/react-devtools-shared/src/devtools/views/hooks.js
index b86fb93b0e01b..f027f751d399f 100644
--- a/packages/react-devtools-shared/src/devtools/views/hooks.js
+++ b/packages/react-devtools-shared/src/devtools/views/hooks.js
@@ -8,11 +8,75 @@
  */
 
 import throttle from 'lodash.throttle';
-import {useCallback, useEffect, useLayoutEffect, useState} from 'react';
+import {
+  useCallback,
+  useEffect,
+  useLayoutEffect,
+  useMemo,
+  useState,
+} from 'react';
+import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
 import {
   localStorageGetItem,
   localStorageSetItem,
 } from 'react-devtools-shared/src/storage';
+import {sanitizeForParse, smartParse, smartStringify} from '../utils';
+
+type EditableValue = {|
+  editableValue: any,
+  hasPendingChanges: boolean,
+  isValid: boolean,
+  parsedValue: any,
+  reset: () => void,
+  update: (newValue: any) => void,
+|};
+
+// Convenience hook for working with an editable value that is validated via JSON.parse.
+export function useEditableValue(
+  initialValue: any,
+  initialIsValid?: boolean = true,
+): EditableValue {
+  const [editableValue, setEditableValue] = useState(() =>
+    smartStringify(initialValue),
+  );
+  const [parsedValue, setParsedValue] = useState(initialValue);
+  const [isValid, setIsValid] = useState(initialIsValid);
+
+  const reset = useCallback(() => {
+    setEditableValue(smartStringify(initialValue));
+    setParsedValue(initialValue);
+    setIsValid(initialIsValid);
+  }, []);
+
+  const update = useCallback(newValue => {
+    let isNewValueValid = false;
+    let newParsedValue;
+    try {
+      newParsedValue = smartParse(newValue);
+      isNewValueValid = true;
+    } catch (error) {}
+
+    batchedUpdates(() => {
+      setEditableValue(sanitizeForParse(newValue));
+      if (isNewValueValid) {
+        setParsedValue(newParsedValue);
+      }
+      setIsValid(isNewValueValid);
+    });
+  }, []);
+
+  return useMemo(
+    () => ({
+      editableValue,
+      hasPendingChanges: smartStringify(initialValue) !== editableValue,
+      isValid,
+      parsedValue,
+      reset,
+      update,
+    }),
+    [editableValue, initialValue, isValid, parsedValue],
+  );
+}
 
 export function useIsOverflowing(
   containerRef: {current: HTMLDivElement | null},