@samitouri / QOS-React-2 / commits / bb2939ccc2

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 {