Support editable useState hooks in DevTools (#14906)
* ReactDebugHooks identifies State and Reducer hooks as editable * Inject overrideHookState() method to DevTools to support editing in DEV builds * Added an integration test for React DevTools, react-debug-tools, and overrideHookState
Brian Vaughn committed
Feb 28, 2019 at 14:37 UTC
bb2939ccc23c9895d798f889d9c32848be43225e
5 files changed
+567
-38
packages/react-debug-tools/src/ReactDebugHooks.js
+20
-1
@@ -232,6 +232,8 @@ const Dispatcher: DispatcherType = {
232
// Inspect
233
234
type HooksNode = {
235
+ id: number | null,
236
+ isStateEditable: boolean,
237
name: string,
238
value: mixed,
239
subHooks: Array<HooksNode>,
@@ -373,6 +375,7 @@ function buildTree(rootStack, readHookLog): HooksTree {
375
let rootChildren = [];
376
let prevStack = null;
377
let levelChildren = rootChildren;
378
+ let nativeHookID = 0;
379
let stackOfChildren = [];
380
for (let i = 0; i < readHookLog.length; i++) {
381
let hook = readHookLog[i];
@@ -403,6 +406,8 @@ function buildTree(rootStack, readHookLog): HooksTree {
406
for (let j = stack.length - commonSteps - 1; j >= 1; j--) {
407
let children = [];
408
levelChildren.push({
409
+ id: null,
410
+ isStateEditable: false,
411
name: parseCustomHookName(stack[j - 1].functionName),
412
value: undefined,
413
subHooks: children,
@@ -412,8 +417,22 @@ function buildTree(rootStack, readHookLog): HooksTree {
417
}
418
prevStack = stack;
419
}
420
+ const {primitive} = hook;
421
+
422
+ // For now, the "id" of stateful hooks is just the stateful hook index.
423
+ // Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
424
+ const id =
425
+ primitive === 'Context' || primitive === 'DebugValue'
426
+ ? null
427
+ : nativeHookID++;
428
+
429
+ // For the time being, only State and Reducer hooks support runtime overrides.
430
+ const isStateEditable = primitive === 'Reducer' || primitive === 'State';
431
+
432
levelChildren.push({
416
- name: hook.primitive,
433
+ id,
434
+ isStateEditable,
435
+ name: primitive,
436
value: hook.value,
437
subHooks: [],
438
});
packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js
new
+176
@@ -0,0 +1,176 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ * @jest-environment node
9
+ */
10
+
11
+'use strict';
12
+
13
+describe('React hooks DevTools integration', () => {
14
+ let React;
15
+ let ReactDebugTools;
16
+ let ReactTestRenderer;
17
+ let act;
18
+ let overrideHookState;
19
+
20
+ beforeEach(() => {
21
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
22
+ inject: injected => {
23
+ overrideHookState = injected.overrideHookState;
24
+ },
25
+ supportsFiber: true,
26
+ onCommitFiberRoot: () => {},
27
+ onCommitFiberUnmount: () => {},
28
+ };
29
+
30
+ jest.resetModules();
31
+
32
+ React = require('react');
33
+ ReactDebugTools = require('react-debug-tools');
34
+ ReactTestRenderer = require('react-test-renderer');
35
+
36
+ act = ReactTestRenderer.act;
37
+ });
38
+
39
+ it('should support editing useState hooks', () => {
40
+ let setCountFn;
41
+
42
+ function MyComponent() {
43
+ const [count, setCount] = React.useState(0);
44
+ setCountFn = setCount;
45
+ return <div>count:{count}</div>;
46
+ }
47
+
48
+ const renderer = ReactTestRenderer.create(<MyComponent />);
49
+ expect(renderer.toJSON()).toEqual({
50
+ type: 'div',
51
+ props: {},
52
+ children: ['count:', '0'],
53
+ });
54
+
55
+ const fiber = renderer.root.findByType(MyComponent)._currentFiber();
56
+ const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
57
+ const stateHook = tree[0];
58
+ expect(stateHook.isStateEditable).toBe(true);
59
+
60
+ if (__DEV__) {
61
+ overrideHookState(fiber, stateHook.id, [], 10);
62
+ expect(renderer.toJSON()).toEqual({
63
+ type: 'div',
64
+ props: {},
65
+ children: ['count:', '10'],
66
+ });
67
+
68
+ act(() => setCountFn(count => count + 1));
69
+ expect(renderer.toJSON()).toEqual({
70
+ type: 'div',
71
+ props: {},
72
+ children: ['count:', '11'],
73
+ });
74
+ }
75
+ });
76
+
77
+ it('should support editable useReducer hooks', () => {
78
+ const initialData = {foo: 'abc', bar: 123};
79
+
80
+ function reducer(state, action) {
81
+ switch (action.type) {
82
+ case 'swap':
83
+ return {foo: state.bar, bar: state.foo};
84
+ default:
85
+ throw new Error();
86
+ }
87
+ }
88
+
89
+ let dispatchFn;
90
+ function MyComponent() {
91
+ const [state, dispatch] = React.useReducer(reducer, initialData);
92
+ dispatchFn = dispatch;
93
+ return (
94
+ <div>
95
+ foo:{state.foo}, bar:{state.bar}
96
+ </div>
97
+ );
98
+ }
99
+
100
+ const renderer = ReactTestRenderer.create(<MyComponent />);
101
+ expect(renderer.toJSON()).toEqual({
102
+ type: 'div',
103
+ props: {},
104
+ children: ['foo:', 'abc', ', bar:', '123'],
105
+ });
106
+
107
+ const fiber = renderer.root.findByType(MyComponent)._currentFiber();
108
+ const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
109
+ const reducerHook = tree[0];
110
+ expect(reducerHook.isStateEditable).toBe(true);
111
+
112
+ if (__DEV__) {
113
+ overrideHookState(fiber, reducerHook.id, ['foo'], 'def');
114
+ expect(renderer.toJSON()).toEqual({
115
+ type: 'div',
116
+ props: {},
117
+ children: ['foo:', 'def', ', bar:', '123'],
118
+ });
119
+
120
+ act(() => dispatchFn({type: 'swap'}));
121
+ expect(renderer.toJSON()).toEqual({
122
+ type: 'div',
123
+ props: {},
124
+ children: ['foo:', '123', ', bar:', 'def'],
125
+ });
126
+ }
127
+ });
128
+
129
+ // This test case is based on an open source bug report:
130
+ // facebookincubator/redux-react-hook/issues/34#issuecomment-466693787
131
+ it('should handle interleaved stateful hooks (e.g. useState) and non-stateful hooks (e.g. useContext)', () => {
132
+ const MyContext = React.createContext(1);
133
+
134
+ let setStateFn;
135
+ function useCustomHook() {
136
+ const context = React.useContext(MyContext);
137
+ const [state, setState] = React.useState({count: context});
138
+ React.useDebugValue(state.count);
139
+ setStateFn = setState;
140
+ return state.count;
141
+ }
142
+
143
+ function MyComponent() {
144
+ const count = useCustomHook();
145
+ return <div>count:{count}</div>;
146
+ }
147
+
148
+ const renderer = ReactTestRenderer.create(<MyComponent />);
149
+ expect(renderer.toJSON()).toEqual({
150
+ type: 'div',
151
+ props: {},
152
+ children: ['count:', '1'],
153
+ });
154
+
155
+ const fiber = renderer.root.findByType(MyComponent)._currentFiber();
156
+ const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
157
+ const stateHook = tree[0].subHooks[1];
158
+ expect(stateHook.isStateEditable).toBe(true);
159
+
160
+ if (__DEV__) {
161
+ overrideHookState(fiber, stateHook.id, ['count'], 10);
162
+ expect(renderer.toJSON()).toEqual({
163
+ type: 'div',
164
+ props: {},
165
+ children: ['count:', '10'],
166
+ });
167
+
168
+ act(() => setStateFn(state => ({count: state.count + 1})));
169
+ expect(renderer.toJSON()).toEqual({
170
+ type: 'div',
171
+ props: {},
172
+ children: ['count:', '11'],
173
+ });
174
+ }
175
+ });
176
+});
packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js
+51
-1
@@ -28,6 +28,8 @@ describe('ReactHooksInspection', () => {
28
let tree = ReactDebugTools.inspectHooks(Foo, {});
29
expect(tree).toEqual([
30
{
31
+ isStateEditable: true,
32
+ id: 0,
33
name: 'State',
34
value: 'hello world',
35
subHooks: [],
@@ -48,10 +50,14 @@ describe('ReactHooksInspection', () => {
50
let tree = ReactDebugTools.inspectHooks(Foo, {});
51
expect(tree).toEqual([
52
{
53
+ isStateEditable: false,
54
+ id: null,
55
name: 'Custom',
56
value: __DEV__ ? 'custom hook label' : undefined,
57
subHooks: [
58
{
59
+ isStateEditable: true,
60
+ id: 0,
61
name: 'State',
62
value: 'hello world',
63
subHooks: [],
@@ -80,15 +86,21 @@ describe('ReactHooksInspection', () => {
86
let tree = ReactDebugTools.inspectHooks(Foo, {});
87
expect(tree).toEqual([
88
{
89
+ isStateEditable: false,
90
+ id: null,
91
name: 'Custom',
92
value: undefined,
93
subHooks: [
94
{
95
+ isStateEditable: true,
96
+ id: 0,
97
name: 'State',
98
subHooks: [],
99
value: 'hello',
100
},
101
{
102
+ isStateEditable: false,
103
+ id: 1,
104
name: 'Effect',
105
subHooks: [],
106
value: effect,
@@ -96,15 +108,21 @@ describe('ReactHooksInspection', () => {
108
],
109
},
110
{
111
+ isStateEditable: false,
112
+ id: null,
113
name: 'Custom',
114
value: undefined,
115
subHooks: [
116
{
117
+ isStateEditable: true,
118
+ id: 2,
119
name: 'State',
120
value: 'world',
121
subHooks: [],
122
},
123
{
124
+ isStateEditable: false,
125
+ id: 3,
126
name: 'Effect',
127
value: effect,
128
subHooks: [],
@@ -143,19 +161,27 @@ describe('ReactHooksInspection', () => {
161
let tree = ReactDebugTools.inspectHooks(Foo, {});
162
expect(tree).toEqual([
163
{
164
+ isStateEditable: false,
165
+ id: null,
166
name: 'Bar',
167
value: undefined,
168
subHooks: [
169
{
170
+ isStateEditable: false,
171
+ id: null,
172
name: 'Custom',
173
value: undefined,
174
subHooks: [
175
{
176
+ isStateEditable: true,
177
+ id: 0,
178
name: 'Reducer',
179
value: 'hello',
180
subHooks: [],
181
},
182
{
183
+ isStateEditable: false,
184
+ id: 1,
185
name: 'Effect',
186
value: effect,
187
subHooks: [],
@@ -163,6 +189,8 @@ describe('ReactHooksInspection', () => {
189
],
190
},
191
{
192
+ isStateEditable: false,
193
+ id: 2,
194
name: 'LayoutEffect',
195
value: effect,
196
subHooks: [],
@@ -170,23 +198,33 @@ describe('ReactHooksInspection', () => {
198
],
199
},
200
{
201
+ isStateEditable: false,
202
+ id: null,
203
name: 'Baz',
204
value: undefined,
205
subHooks: [
206
{
207
+ isStateEditable: false,
208
+ id: 3,
209
name: 'LayoutEffect',
210
value: effect,
211
subHooks: [],
212
},
213
{
214
+ isStateEditable: false,
215
+ id: null,
216
name: 'Custom',
217
subHooks: [
218
{
219
+ isStateEditable: true,
220
+ id: 4,
221
name: 'Reducer',
222
subHooks: [],
223
value: 'world',
224
},
225
{
226
+ isStateEditable: false,
227
+ id: 5,
228
name: 'Effect',
229
subHooks: [],
230
value: effect,
@@ -208,6 +246,8 @@ describe('ReactHooksInspection', () => {
246
let tree = ReactDebugTools.inspectHooks(Foo, {});
247
expect(tree).toEqual([
248
{
249
+ isStateEditable: false,
250
+ id: null,
251
name: 'Context',
252
value: 'default',
253
subHooks: [],
@@ -270,9 +310,19 @@ describe('ReactHooksInspection', () => {
310
let tree = ReactDebugTools.inspectHooks(Foo, {});
311
expect(tree).toEqual([
312
{
313
+ isStateEditable: false,
314
+ id: null,
315
name: 'Custom',
316
value: __DEV__ ? 'bar:123' : undefined,
275
- subHooks: [{name: 'State', subHooks: [], value: 0}],
317
+ subHooks: [
318
+ {
319
+ isStateEditable: true,
320
+ id: 0,
321
+ name: 'State',
322
+ subHooks: [],
323
+ value: 0,
324
+ },
325
+ ],
326
},
327
]);
328
});
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+286
-36
@@ -40,8 +40,20 @@ describe('ReactHooksInspectionIntegration', () => {
40
let childFiber = renderer.root.findByType(Foo)._currentFiber();
41
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
42
expect(tree).toEqual([
43
- {name: 'State', value: 'hello', subHooks: []},
44
- {name: 'State', value: 'world', subHooks: []},
43
+ {
44
+ isStateEditable: true,
45
+ id: 0,
46
+ name: 'State',
47
+ value: 'hello',
48
+ subHooks: [],
49
+ },
50
+ {
51
+ isStateEditable: true,
52
+ id: 1,
53
+ name: 'State',
54
+ value: 'world',
55
+ subHooks: [],
56
+ },
57
]);
58
59
let {
@@ -55,8 +67,20 @@ describe('ReactHooksInspectionIntegration', () => {
67
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
68
69
expect(tree).toEqual([
58
- {name: 'State', value: 'Hi', subHooks: []},
59
- {name: 'State', value: 'world', subHooks: []},
70
+ {
71
+ isStateEditable: true,
72
+ id: 0,
73
+ name: 'State',
74
+ value: 'Hi',
75
+ subHooks: [],
76
+ },
77
+ {
78
+ isStateEditable: true,
79
+ id: 1,
80
+ name: 'State',
81
+ value: 'world',
82
+ subHooks: [],
83
+ },
84
]);
85
86
act(() => setStateB('world!'));
@@ -65,8 +89,20 @@ describe('ReactHooksInspectionIntegration', () => {
89
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
90
91
expect(tree).toEqual([
68
- {name: 'State', value: 'Hi', subHooks: []},
69
- {name: 'State', value: 'world!', subHooks: []},
92
+ {
93
+ isStateEditable: true,
94
+ id: 0,
95
+ name: 'State',
96
+ value: 'Hi',
97
+ subHooks: [],
98
+ },
99
+ {
100
+ isStateEditable: true,
101
+ id: 1,
102
+ name: 'State',
103
+ value: 'world!',
104
+ subHooks: [],
105
+ },
106
]);
107
});
108
@@ -116,14 +152,56 @@ describe('ReactHooksInspectionIntegration', () => {
152
153
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
154
expect(tree).toEqual([
119
- {name: 'State', value: 'a', subHooks: []},
120
- {name: 'Reducer', value: 'b', subHooks: []},
121
- {name: 'Ref', value: 'c', subHooks: []},
122
- {name: 'LayoutEffect', value: effect, subHooks: []},
123
- {name: 'Effect', value: effect, subHooks: []},
124
- {name: 'ImperativeHandle', value: outsideRef.current, subHooks: []},
125
- {name: 'Memo', value: 'ab', subHooks: []},
126
- {name: 'Callback', value: updateStates, subHooks: []},
155
+ {
156
+ isStateEditable: true,
157
+ id: 0,
158
+ name: 'State',
159
+ value: 'a',
160
+ subHooks: [],
161
+ },
162
+ {
163
+ isStateEditable: true,
164
+ id: 1,
165
+ name: 'Reducer',
166
+ value: 'b',
167
+ subHooks: [],
168
+ },
169
+ {isStateEditable: false, id: 2, name: 'Ref', value: 'c', subHooks: []},
170
+ {
171
+ isStateEditable: false,
172
+ id: 3,
173
+ name: 'LayoutEffect',
174
+ value: effect,
175
+ subHooks: [],
176
+ },
177
+ {
178
+ isStateEditable: false,
179
+ id: 4,
180
+ name: 'Effect',
181
+ value: effect,
182
+ subHooks: [],
183
+ },
184
+ {
185
+ isStateEditable: false,
186
+ id: 5,
187
+ name: 'ImperativeHandle',
188
+ value: outsideRef.current,
189
+ subHooks: [],
190
+ },
191
+ {
192
+ isStateEditable: false,
193
+ id: 6,
194
+ name: 'Memo',
195
+ value: 'ab',
196
+ subHooks: [],
197
+ },
198
+ {
199
+ isStateEditable: false,
200
+ id: 7,
201
+ name: 'Callback',
202
+ value: updateStates,
203
+ subHooks: [],
204
+ },
205
]);
206
207
updateStates();
@@ -132,14 +210,56 @@ describe('ReactHooksInspectionIntegration', () => {
210
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
211
212
expect(tree).toEqual([
135
- {name: 'State', value: 'A', subHooks: []},
136
- {name: 'Reducer', value: 'B', subHooks: []},
137
- {name: 'Ref', value: 'C', subHooks: []},
138
- {name: 'LayoutEffect', value: effect, subHooks: []},
139
- {name: 'Effect', value: effect, subHooks: []},
140
- {name: 'ImperativeHandle', value: outsideRef.current, subHooks: []},
141
- {name: 'Memo', value: 'Ab', subHooks: []},
142
- {name: 'Callback', value: updateStates, subHooks: []},
213
+ {
214
+ isStateEditable: true,
215
+ id: 0,
216
+ name: 'State',
217
+ value: 'A',
218
+ subHooks: [],
219
+ },
220
+ {
221
+ isStateEditable: true,
222
+ id: 1,
223
+ name: 'Reducer',
224
+ value: 'B',
225
+ subHooks: [],
226
+ },
227
+ {isStateEditable: false, id: 2, name: 'Ref', value: 'C', subHooks: []},
228
+ {
229
+ isStateEditable: false,
230
+ id: 3,
231
+ name: 'LayoutEffect',
232
+ value: effect,
233
+ subHooks: [],
234
+ },
235
+ {
236
+ isStateEditable: false,
237
+ id: 4,
238
+ name: 'Effect',
239
+ value: effect,
240
+ subHooks: [],
241
+ },
242
+ {
243
+ isStateEditable: false,
244
+ id: 5,
245
+ name: 'ImperativeHandle',
246
+ value: outsideRef.current,
247
+ subHooks: [],
248
+ },
249
+ {
250
+ isStateEditable: false,
251
+ id: 6,
252
+ name: 'Memo',
253
+ value: 'Ab',
254
+ subHooks: [],
255
+ },
256
+ {
257
+ isStateEditable: false,
258
+ id: 7,
259
+ name: 'Callback',
260
+ value: updateStates,
261
+ subHooks: [],
262
+ },
263
]);
264
});
265
@@ -158,6 +278,8 @@ describe('ReactHooksInspectionIntegration', () => {
278
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
279
expect(tree).toEqual([
280
{
281
+ isStateEditable: false,
282
+ id: null,
283
name: 'Context',
284
value: 'contextual',
285
subHooks: [],
@@ -177,7 +299,13 @@ describe('ReactHooksInspectionIntegration', () => {
299
let childFiber = renderer.root.findByType(Foo)._currentFiber();
300
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
301
expect(tree).toEqual([
180
- {name: 'ImperativeHandle', value: obj, subHooks: []},
302
+ {
303
+ isStateEditable: false,
304
+ id: 0,
305
+ name: 'ImperativeHandle',
306
+ value: obj,
307
+ subHooks: [],
308
+ },
309
]);
310
});
311
@@ -191,7 +319,15 @@ describe('ReactHooksInspectionIntegration', () => {
319
// TODO: Test renderer findByType is broken for memo. Have to search for the inner.
320
let childFiber = renderer.root.findByType(InnerFoo)._currentFiber();
321
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
194
- expect(tree).toEqual([{name: 'State', value: 'hello', subHooks: []}]);
322
+ expect(tree).toEqual([
323
+ {
324
+ isStateEditable: true,
325
+ id: 0,
326
+ name: 'State',
327
+ value: 'hello',
328
+ subHooks: [],
329
+ },
330
+ ]);
331
});
332
333
it('should inspect custom hooks', () => {
@@ -208,9 +344,19 @@ describe('ReactHooksInspectionIntegration', () => {
344
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
345
expect(tree).toEqual([
346
{
347
+ isStateEditable: false,
348
+ id: null,
349
name: 'Custom',
350
value: undefined,
213
- subHooks: [{name: 'State', value: 'hello', subHooks: []}],
351
+ subHooks: [
352
+ {
353
+ isStateEditable: true,
354
+ id: 0,
355
+ name: 'State',
356
+ value: 'hello',
357
+ subHooks: [],
358
+ },
359
+ ],
360
},
361
]);
362
});
@@ -238,24 +384,56 @@ describe('ReactHooksInspectionIntegration', () => {
384
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
385
expect(tree).toEqual([
386
{
387
+ isStateEditable: false,
388
+ id: null,
389
name: 'LabeledValue',
390
value: __DEV__ ? 'custom label a' : undefined,
243
- subHooks: [{name: 'State', value: 'a', subHooks: []}],
391
+ subHooks: [
392
+ {
393
+ isStateEditable: true,
394
+ id: 0,
395
+ name: 'State',
396
+ value: 'a',
397
+ subHooks: [],
398
+ },
399
+ ],
400
},
401
{
402
+ isStateEditable: true,
403
+ id: 1,
404
name: 'State',
405
value: 'b',
406
subHooks: [],
407
},
408
{
409
+ isStateEditable: false,
410
+ id: null,
411
name: 'Anonymous',
412
value: undefined,
253
- subHooks: [{name: 'State', value: 'c', subHooks: []}],
413
+ subHooks: [
414
+ {
415
+ isStateEditable: true,
416
+ id: 2,
417
+ name: 'State',
418
+ value: 'c',
419
+ subHooks: [],
420
+ },
421
+ ],
422
},
423
{
424
+ isStateEditable: false,
425
+ id: null,
426
name: 'LabeledValue',
427
value: __DEV__ ? 'custom label d' : undefined,
258
- subHooks: [{name: 'State', value: 'd', subHooks: []}],
428
+ subHooks: [
429
+ {
430
+ isStateEditable: true,
431
+ id: 3,
432
+ name: 'State',
433
+ value: 'd',
434
+ subHooks: [],
435
+ },
436
+ ],
437
},
438
]);
439
});
@@ -278,13 +456,25 @@ describe('ReactHooksInspectionIntegration', () => {
456
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
457
expect(tree).toEqual([
458
{
459
+ isStateEditable: false,
460
+ id: null,
461
name: 'Outer',
462
value: __DEV__ ? 'outer' : undefined,
463
subHooks: [
464
{
465
+ isStateEditable: false,
466
+ id: null,
467
name: 'Inner',
468
value: __DEV__ ? 'inner' : undefined,
287
- subHooks: [{name: 'State', value: 0, subHooks: []}],
469
+ subHooks: [
470
+ {
471
+ isStateEditable: true,
472
+ id: 0,
473
+ name: 'State',
474
+ value: 0,
475
+ subHooks: [],
476
+ },
477
+ ],
478
},
479
],
480
},
@@ -313,19 +503,49 @@ describe('ReactHooksInspectionIntegration', () => {
503
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
504
expect(tree).toEqual([
505
{
506
+ isStateEditable: false,
507
+ id: null,
508
name: 'SingleLabelCustom',
509
value: __DEV__ ? 'single one' : undefined,
318
- subHooks: [{name: 'State', value: 0, subHooks: []}],
510
+ subHooks: [
511
+ {
512
+ isStateEditable: true,
513
+ id: 0,
514
+ name: 'State',
515
+ value: 0,
516
+ subHooks: [],
517
+ },
518
+ ],
519
},
520
{
521
+ isStateEditable: false,
522
+ id: null,
523
name: 'MultiLabelCustom',
524
value: __DEV__ ? ['one', 'two', 'three'] : undefined,
323
- subHooks: [{name: 'State', value: 0, subHooks: []}],
525
+ subHooks: [
526
+ {
527
+ isStateEditable: true,
528
+ id: 1,
529
+ name: 'State',
530
+ value: 0,
531
+ subHooks: [],
532
+ },
533
+ ],
534
},
535
{
536
+ isStateEditable: false,
537
+ id: null,
538
name: 'SingleLabelCustom',
539
value: __DEV__ ? 'single two' : undefined,
328
- subHooks: [{name: 'State', value: 0, subHooks: []}],
540
+ subHooks: [
541
+ {
542
+ isStateEditable: true,
543
+ id: 2,
544
+ name: 'State',
545
+ value: 0,
546
+ subHooks: [],
547
+ },
548
+ ],
549
},
550
]);
551
});
@@ -355,9 +575,19 @@ describe('ReactHooksInspectionIntegration', () => {
575
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
576
expect(tree).toEqual([
577
{
578
+ isStateEditable: false,
579
+ id: null,
580
name: 'Custom',
581
value: __DEV__ ? 'bar:123' : undefined,
360
- subHooks: [{name: 'State', subHooks: [], value: 0}],
582
+ subHooks: [
583
+ {
584
+ isStateEditable: true,
585
+ id: 0,
586
+ name: 'State',
587
+ subHooks: [],
588
+ value: 0,
589
+ },
590
+ ],
591
},
592
]);
593
});
@@ -390,7 +620,15 @@ describe('ReactHooksInspectionIntegration', () => {
620
621
let childFiber = renderer.root._currentFiber();
622
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
393
- expect(tree).toEqual([{name: 'State', value: 'def', subHooks: []}]);
623
+ expect(tree).toEqual([
624
+ {
625
+ isStateEditable: true,
626
+ id: 0,
627
+ name: 'State',
628
+ value: 'def',
629
+ subHooks: [],
630
+ },
631
+ ]);
632
});
633
634
it('should support an injected dispatcher', () => {
@@ -461,8 +699,20 @@ describe('ReactHooksInspectionIntegration', () => {
699
const childFiber = renderer.root._currentFiber();
700
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
701
expect(tree).toEqual([
464
- {name: 'Context', value: 1, subHooks: []},
465
- {name: 'State', value: {count: 2}, subHooks: []},
702
+ {
703
+ isStateEditable: false,
704
+ id: null,
705
+ name: 'Context',
706
+ value: 1,
707
+ subHooks: [],
708
+ },
709
+ {
710
+ isStateEditable: true,
711
+ id: 0,
712
+ name: 'State',
713
+ value: {count: 2},
714
+ subHooks: [],
715
+ },
716
]);
717
});
718
});
packages/react-reconciler/src/ReactFiberReconciler.js
+34
@@ -341,6 +341,7 @@ export function findHostInstanceWithNoPortals(
341
return hostFiber.stateNode;
342
}
343
344
+let overrideHookState = null;
345
let overrideProps = null;
346
347
if (__DEV__) {
@@ -368,6 +369,38 @@ if (__DEV__) {
369
return copyWithSetImpl(obj, path, 0, value);
370
};
371
372
+ // Support DevTools editable values for useState and useReducer.
373
+ overrideHookState = (
374
+ fiber: Fiber,
375
+ id: number,
376
+ path: Array<string | number>,
377
+ value: any,
378
+ ) => {
379
+ // For now, the "id" of stateful hooks is just the stateful hook index.
380
+ // This may change in the future with e.g. nested hooks.
381
+ let currentHook = fiber.memoizedState;
382
+ while (currentHook !== null && id > 0) {
383
+ currentHook = currentHook.next;
384
+ id--;
385
+ }
386
+ if (currentHook !== null) {
387
+ flushPassiveEffects();
388
+
389
+ const newState = copyWithSet(currentHook.memoizedState, path, value);
390
+ currentHook.memoizedState = newState;
391
+ currentHook.baseState = newState;
392
+
393
+ // We aren't actually adding an update to the queue,
394
+ // because there is no update we can add for useReducer hooks that won't trigger an error.
395
+ // (There's no appropriate action type for DevTools overrides.)
396
+ // As a result though, React will see the scheduled update as a noop and bailout.
397
+ // Shallow cloning props works as a workaround for now to bypass the bailout check.
398
+ fiber.memoizedProps = {...fiber.memoizedProps};
399
+
400
+ scheduleWork(fiber, Sync);
401
+ }
402
+ };
403
+
404
// Support DevTools props for function components, forwardRef, memo, host components, etc.
405
overrideProps = (fiber: Fiber, path: Array<string | number>, value: any) => {
406
flushPassiveEffects();
@@ -385,6 +418,7 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
418
419
return injectInternals({
420
...devToolsConfig,
421
+ overrideHookState,
422
overrideProps,
423
currentDispatcherRef: ReactCurrentDispatcher,
424
findHostInstanceByFiber(fiber: Fiber): Instance | TextInstance | null {