Added support for editable hook values (pending facebook/react/pull/14906)
Brian Vaughn committed
Feb 20, 2019 at 15:58 UTC
8cd8b29015646b65f7db7c1b39a0c9afa19612c9
11 files changed
+206
-46
src/backend/ReactDebugHooks.js
+7
-3
@@ -225,6 +225,7 @@ type ReactCurrentDispatcher = {
225
};
226
227
type HooksNode = {
228
+ nativeHookIndex: number,
229
name: string,
230
value: mixed,
231
subHooks: Array<HooksNode>,
@@ -366,6 +367,7 @@ function buildTree(rootStack, readHookLog): HooksTree {
367
const rootChildren = [];
368
let prevStack = null;
369
let levelChildren = rootChildren;
370
+ let nativeHookIndex = 0;
371
const stackOfChildren = [];
372
for (let i = 0; i < readHookLog.length; i++) {
373
const hook = readHookLog[i];
@@ -399,6 +401,7 @@ function buildTree(rootStack, readHookLog): HooksTree {
401
levelChildren.push({
402
name: parseCustomHookName(stack[j - 1].functionName),
403
value: undefined,
404
+ nativeHookIndex: -1,
405
subHooks: children,
406
});
407
stackOfChildren.push(levelChildren);
@@ -409,12 +412,13 @@ function buildTree(rootStack, readHookLog): HooksTree {
412
levelChildren.push({
413
name: hook.primitive,
414
value: hook.value,
415
+ nativeHookIndex: hook.primitive === 'DebugValue' ? -1 : nativeHookIndex++,
416
subHooks: [],
417
});
418
}
419
420
// Associate custom hook values (useDebugValue() hook entries) with the correct hooks.
417
- rollupDebugValues(rootChildren, null);
421
+ processDebugValues(rootChildren, null);
422
423
return rootChildren;
424
}
@@ -423,7 +427,7 @@ function buildTree(rootStack, readHookLog): HooksTree {
427
// That hook adds the user-provided values to the hooks tree.
428
// This method removes those values (so they don't appear in DevTools),
429
// and bubbles them up to the "value" attribute of their parent custom hook.
426
-function rollupDebugValues(
430
+function processDebugValues(
431
hooksTree: HooksTree,
432
parentHooksNode: HooksNode | null
433
): void {
@@ -436,7 +440,7 @@ function rollupDebugValues(
440
i--;
441
debugValueHooksNodes.push(hooksNode);
442
} else {
439
- rollupDebugValues(hooksNode.subHooks, hooksNode);
443
+ processDebugValues(hooksNode.subHooks, hooksNode);
444
}
445
}
446
src/backend/agent.js
+24
@@ -23,6 +23,14 @@ type InspectSelectParams = {|
23
rendererID: number,
24
|};
25
26
+type OverrideHookParams = {|
27
+ id: number,
28
+ nativeHookIndex: number,
29
+ path: Array<string | number>,
30
+ rendererID: number,
31
+ value: any,
32
+|};
33
+
34
type SetInParams = {|
35
id: number,
36
path: Array<string | number>,
@@ -40,6 +48,7 @@ export default class Agent extends EventEmitter {
48
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
49
bridge.addListener('inspectElement', this.inspectElement);
50
bridge.addListener('overrideContext', this.overrideContext);
51
+ bridge.addListener('overrideHook', this.overrideHook);
52
bridge.addListener('overrideProps', this.overrideProps);
53
bridge.addListener('overrideState', this.overrideState);
54
bridge.addListener('selectElement', this.selectElement);
@@ -122,6 +131,21 @@ export default class Agent extends EventEmitter {
131
}
132
};
133
134
+ overrideHook = ({
135
+ id,
136
+ nativeHookIndex,
137
+ path,
138
+ rendererID,
139
+ value,
140
+ }: OverrideHookParams) => {
141
+ const renderer = this._rendererInterfaces[rendererID];
142
+ if (renderer == null) {
143
+ console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
144
+ } else {
145
+ renderer.setInHook(id, nativeHookIndex, path, value);
146
+ }
147
+ };
148
+
149
overrideProps = ({ id, path, rendererID, value }: SetInParams) => {
150
const renderer = this._rendererInterfaces[rendererID];
151
if (renderer == null) {
src/backend/index.js
+6
-2
@@ -1,11 +1,15 @@
1
// @flow
2
3
-import type { Hook, ReactRenderer, RendererInterface } from './types';
3
+import type { DevToolsHook, ReactRenderer, RendererInterface } from './types';
4
import Agent from './agent';
5
6
import { attach } from './renderer';
7
8
-export function initBackend(hook: Hook, agent: Agent, global: Object): void {
8
+export function initBackend(
9
+ hook: DevToolsHook,
10
+ agent: Agent,
11
+ global: Object
12
+): void {
13
const subs = [
14
hook.sub(
15
'renderer-attached',
src/backend/renderer.js
+22
-4
@@ -24,8 +24,8 @@ import { getUID } from '../utils';
24
import { inspectHooksOfFiber } from './ReactDebugHooks';
25
26
import type {
27
+ DevToolsHook,
28
Fiber,
28
- Hook,
29
ReactRenderer,
30
FiberData,
31
RendererInterface,
@@ -151,7 +151,7 @@ function getInternalReactConstants(version) {
151
}
152
153
export function attach(
154
- hook: Hook,
154
+ hook: DevToolsHook,
155
rendererID: number,
156
renderer: ReactRenderer,
157
global: Object
@@ -194,7 +194,7 @@ export function attach(
194
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
195
} = ReactSymbols;
196
197
- const { overrideProps } = renderer;
197
+ const { overrideHook, overrideProps } = renderer;
198
199
const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
200
if (__DEBUG__) {
@@ -1141,7 +1141,10 @@ export function attach(
1141
return {
1142
id,
1143
1144
- // Does the current renderer support editable props/state/hooks?
1144
+ // Does the current renderer support editable hooks?
1145
+ canEditHooks: typeof overrideHook === 'function',
1146
+
1147
+ // Does the current renderer support editable function props?
1148
canEditFunctionProps: typeof overrideProps === 'function',
1149
1150
// Inspectable properties.
@@ -1163,6 +1166,20 @@ export function attach(
1166
};
1167
}
1168
1169
+ function setInHook(
1170
+ id: number,
1171
+ nativeHookIndex: number,
1172
+ path: Array<string | number>,
1173
+ value: any
1174
+ ) {
1175
+ const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1176
+ if (fiber !== null) {
1177
+ if (typeof overrideHook === 'function') {
1178
+ overrideHook(fiber, nativeHookIndex, path, value);
1179
+ }
1180
+ }
1181
+ }
1182
+
1183
function setInProps(id: number, path: Array<string | number>, value: any) {
1184
const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1185
if (fiber !== null) {
@@ -1216,6 +1233,7 @@ export function attach(
1233
cleanup,
1234
renderer,
1235
setInContext,
1236
+ setInHook,
1237
setInProps,
1238
setInState,
1239
walkTree,
src/backend/types.js
+19
-2
@@ -33,6 +33,16 @@ export type ReactRenderer = {
33
findFiberByHostInstance: (hostInstance: NativeType) => ?Fiber,
34
version: string,
35
bundleType: BundleType,
36
+
37
+ // 16.9+
38
+ overrideHook?: ?(
39
+ fiber: Object,
40
+ nativeHookIndex: number,
41
+ path: Array<string | number>,
42
+ value: any
43
+ ) => void,
44
+
45
+ // 16.7+
46
overrideProps?: ?(
47
fiber: Object,
48
path: Array<string | number>,
@@ -55,15 +65,21 @@ export type RendererInterface = {
65
inspectElement: (id: number) => InspectedElement | null,
66
renderer: ReactRenderer | null,
67
selectElement: (id: number) => void,
68
+ setInContext: (id: number, path: Array<string | number>, value: any) => void,
69
+ setInHook: (
70
+ id: number,
71
+ nativeHookIndex: number,
72
+ path: Array<string | number>,
73
+ value: any
74
+ ) => void,
75
setInProps: (id: number, path: Array<string | number>, value: any) => void,
76
setInState: (id: number, path: Array<string | number>, value: any) => void,
60
- setInContext: (id: number, path: Array<string | number>, value: any) => void,
77
walkTree: () => void,
78
};
79
80
export type Handler = (data: any) => void;
81
66
-export type Hook = {
82
+export type DevToolsHook = {
83
listeners: { [key: string]: Array<Handler> },
84
rendererInterfaces: Map<RendererID, RendererInterface>,
85
renderers: Map<RendererID, ReactRenderer>,
@@ -83,6 +99,7 @@ export type Hook = {
99
};
100
101
export type HooksNode = {
102
+ nativeHookIndex: number,
103
name: string,
104
value: mixed,
105
subHooks: Array<HooksNode>,
src/devtools/types.js
+3
@@ -48,6 +48,9 @@ export type Owner = {|
48
export type InspectedElement = {|
49
id: number,
50
51
+ // Does the current renderer support editable hooks?
52
+ canEditHooks: boolean,
53
+
54
// Does the current renderer support editable function props?
55
canEditFunctionProps: boolean,
56
src/devtools/views/HooksTree.css
+8
-1
@@ -3,15 +3,22 @@
3
border-top: 1px solid var(--color-border);
4
}
5
6
-.HooksNode {
6
+.Hook {
7
padding-left: 0.75rem;
8
}
9
10
.NameValueRow {
11
+ display: flex;
12
}
13
14
.Name {
15
color: var(--color-attribute-name);
16
+ flex: 0 0 auto;
17
+}
18
+.Name:after {
19
+ content: ': ';
20
+ color: var(--color-text-color);
21
+ margin-right: 0.5rem;
22
}
23
24
.Value {
src/devtools/views/HooksTree.js
+95
-24
@@ -1,36 +1,67 @@
1
// @flow
2
3
-import React from 'react';
4
-import { KeyValue } from './InspectedElementTree';
3
+import React, { useContext } from 'react';
4
+import { BridgeContext, StoreContext } from './context';
5
+import { EditableValue, KeyValue } from './InspectedElementTree';
6
import styles from './HooksTree.css';
7
8
import type { HooksNode, HooksTree } from 'src/backend/types';
9
9
-export function HooksTreeView({ hooksTree }: { hooksTree: HooksTree | null }) {
10
- if (hooksTree === null) {
10
+type HooksTreeViewProps = {|
11
+ canEditHooks: boolean,
12
+ hooks: HooksTree | null,
13
+ id: number,
14
+|};
15
+
16
+export function HooksTreeView({ canEditHooks, hooks, id }: HooksTreeViewProps) {
17
+ if (hooks === null) {
18
return null;
19
} else {
20
return (
21
<div className={styles.HooksTreeView}>
22
<div className={styles.Item}>hooks</div>
16
- <InnerHooksTreeView hooksTree={hooksTree} />
23
+ <InnerHooksTreeView canEditHooks={canEditHooks} hooks={hooks} id={id} />
24
</div>
25
);
26
}
27
}
28
22
-export function InnerHooksTreeView({ hooksTree }: { hooksTree: HooksTree }) {
29
+type InnerHooksTreeViewProps = {|
30
+ canEditHooks: boolean,
31
+ hooks: HooksTree,
32
+ id: number,
33
+|};
34
+
35
+export function InnerHooksTreeView({
36
+ canEditHooks,
37
+ hooks,
38
+ id,
39
+}: InnerHooksTreeViewProps) {
40
// $FlowFixMe "Missing type annotation for U" whatever that means
24
- return hooksTree.map((hooksNode, index) => (
25
- <HooksNodeView key={index} hooksNode={hooksTree[index]} />
41
+ return hooks.map((hook, index) => (
42
+ <HookView
43
+ key={index}
44
+ canEditHooks={canEditHooks}
45
+ hook={hooks[index]}
46
+ id={id}
47
+ />
48
));
49
}
50
29
-function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) {
30
- const { name, subHooks, value } = hooksNode;
51
+type HookViewProps = {|
52
+ canEditHooks: boolean,
53
+ hook: HooksNode,
54
+ id: number,
55
+ path?: Array<any>,
56
+|};
57
+
58
+function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
59
+ const { name, nativeHookIndex, subHooks, value } = hook;
60
+
61
+ const bridge = useContext(BridgeContext);
62
+ const store = useContext(StoreContext);
63
64
// TODO Add click and key handlers for toggling element open/close state.
33
- // TODO Support editable props
65
66
const isCustomHook = subHooks.length > 0;
67
@@ -40,7 +71,11 @@ function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) {
71
let isComplexDisplayValue = false;
72
73
// Format data for display to mimic the props/state/context for now.
43
- if (type === 'number' || type === 'string' || type === 'boolean') {
74
+ if (type === 'string') {
75
+ displayValue = `"${((value: any): string)}"`;
76
+ } else if (type === 'boolean') {
77
+ displayValue = value ? 'true' : 'false';
78
+ } else if (type === 'number') {
79
displayValue = value;
80
} else if (value === null) {
81
displayValue = 'null';
@@ -57,39 +92,75 @@ function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) {
92
if (isCustomHook) {
93
if (isComplexDisplayValue) {
94
return (
60
- <div className={styles.HooksNode}>
95
+ <div className={styles.Hook}>
96
<div className={styles.NameValueRow}>
62
- <span className={styles.Name}>{name}: </span>
97
+ <span className={styles.Name}>{name}</span>
98
</div>
99
<KeyValue depth={1} name="DebugValue" value={value} />
65
- <InnerHooksTreeView hooksTree={subHooks} />
100
+ <InnerHooksTreeView
101
+ canEditHooks={canEditHooks}
102
+ hooks={subHooks}
103
+ id={id}
104
+ />
105
</div>
106
);
107
} else {
108
return (
70
- <div className={styles.HooksNode}>
109
+ <div className={styles.Hook}>
110
<div className={styles.NameValueRow}>
72
- <span className={styles.Name}>{name}: </span> {/* $FlowFixMe */}
111
+ <span className={styles.Name}>{name}</span> {/* $FlowFixMe */}
112
<span className={styles.Value}>{displayValue}</span>
113
</div>
75
- <InnerHooksTreeView hooksTree={subHooks} />
114
+ <InnerHooksTreeView
115
+ canEditHooks={canEditHooks}
116
+ hooks={subHooks}
117
+ id={id}
118
+ />
119
</div>
120
);
121
}
122
} else {
123
+ let overrideValueFn = null;
124
+ if (canEditHooks && name === 'State') {
125
+ overrideValueFn = (path: Array<string | number>, value: any) => {
126
+ const rendererID = store.getRendererIDForElement(id);
127
+ bridge.send('overrideHook', {
128
+ id,
129
+ nativeHookIndex,
130
+ path,
131
+ rendererID,
132
+ value,
133
+ });
134
+ };
135
+ }
136
+
137
if (isComplexDisplayValue) {
138
return (
82
- <div className={styles.HooksNode}>
83
- <KeyValue depth={0} name={name} value={value} />
139
+ <div className={styles.Hook}>
140
+ <KeyValue
141
+ depth={0}
142
+ name={name}
143
+ overrideValueFn={overrideValueFn}
144
+ value={value}
145
+ />
146
</div>
147
);
148
} else {
149
return (
88
- <div className={styles.HooksNode}>
150
+ <div className={styles.Hook}>
151
<div className={styles.NameValueRow}>
90
- <span className={styles.Name}>{name}: </span>
91
- {/* $FlowFixMe */}
92
- <span className={styles.Value}>{displayValue}</span>
152
+ <span className={styles.Name}>{name}</span>
153
+ {typeof overrideValueFn === 'function' ? (
154
+ <EditableValue
155
+ dataType={type}
156
+ overrideValueFn={overrideValueFn}
157
+ path={[]}
158
+ value={value}
159
+ />
160
+ ) : (
161
+ // $FlowFixMe Cannot create span element because in property children
162
+ <span className={styles.Value}>{displayValue}</span>
163
+ )}
164
</div>
165
</div>
166
);
src/devtools/views/InspectedElementTree.js
+8
-3
@@ -26,7 +26,6 @@ export default function InspectedElementTree({
26
return null;
27
} else {
28
// TODO Add click and key handlers for toggling element open/close state.
29
- // TODO Support editable props
29
return (
30
<div className={styles.InspectedElementTree}>
31
<div className={styles.Item}>{label}</div>
@@ -164,7 +163,7 @@ type EditableValueProps = {|
163
value: any,
164
|};
165
167
-function EditableValue({
166
+export function EditableValue({
167
dataType,
168
overrideValueFn,
169
path,
@@ -214,7 +213,13 @@ function EditableValue({
213
onKeyDown={handleKeyDown}
214
onKeyPress={handleKeyPress}
215
type={type}
217
- value={dataType === 'boolean' ? undefined : editableValue || ''}
216
+ value={
217
+ dataType === 'boolean'
218
+ ? undefined
219
+ : editableValue != null
220
+ ? editableValue
221
+ : ''
222
+ }
223
/>
224
</label>
225
);
src/devtools/views/SelectedElement.js
+11
-4
@@ -107,7 +107,15 @@ function InspectedElementView({
107
inspectedElement,
108
}: InspectedElementViewProps) {
109
const { id, type } = element;
110
- const { context, hooks, owners, props, state } = inspectedElement;
110
+ const {
111
+ canEditFunctionProps,
112
+ canEditHooks,
113
+ context,
114
+ hooks,
115
+ owners,
116
+ props,
117
+ state,
118
+ } = inspectedElement;
119
120
const { ownerStack } = useContext(TreeContext);
121
const bridge = useContext(BridgeContext);
@@ -129,8 +137,7 @@ function InspectedElementView({
137
const rendererID = store.getRendererIDForElement(id);
138
bridge.send('overrideState', { id, path, rendererID, value });
139
};
132
- } else if (type === ElementTypeFunction) {
133
- // TODO Only enable this if renderer.canEditFunctionProps is true!
140
+ } else if (type === ElementTypeFunction && canEditFunctionProps) {
141
overridePropsFn = (path: Array<string | number>, value: any) => {
142
const rendererID = store.getRendererIDForElement(id);
143
bridge.send('overrideProps', { id, path, rendererID, value });
@@ -150,7 +157,7 @@ function InspectedElementView({
157
data={state}
158
overrideValueFn={overrideStateFn}
159
/>
153
- <HooksTree hooksTree={hooks} />
160
+ <HooksTree canEditHooks={canEditHooks} hooks={hooks} id={id} />
161
<InspectedElementTree
162
label="context"
163
data={context}
src/hook.js
+3
-3
@@ -7,11 +7,11 @@
7
* @flow
8
*/
9
10
-import type { Hook } from 'src/backend/types';
10
+import type { DevToolsHook } from 'src/backend/types';
11
12
declare var window: any;
13
14
-export function installHook(target: any): Hook | null {
14
+export function installHook(target: any): DevToolsHook | null {
15
if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) return null;
16
17
function detectReactBuildType(renderer) {
@@ -155,7 +155,7 @@ export function installHook(target: any): Hook | null {
155
const listeners = {};
156
const renderers = new Map();
157
158
- const hook: Hook = {
158
+ const hook: DevToolsHook = {
159
rendererInterfaces,
160
listeners,
161
renderers,