Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support editable useState hooks in DevTools #14906

Merged
merged 10 commits into from
Feb 28, 2019
21 changes: 20 additions & 1 deletion packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ const Dispatcher: DispatcherType = {
// Inspect

type HooksNode = {
id: number | null,
isStateEditable: boolean,
name: string,
value: mixed,
subHooks: Array<HooksNode>,
Expand Down Expand Up @@ -373,6 +375,7 @@ function buildTree(rootStack, readHookLog): HooksTree {
let rootChildren = [];
let prevStack = null;
let levelChildren = rootChildren;
let nativeHookID = 0;
let stackOfChildren = [];
for (let i = 0; i < readHookLog.length; i++) {
let hook = readHookLog[i];
Expand Down Expand Up @@ -403,6 +406,8 @@ function buildTree(rootStack, readHookLog): HooksTree {
for (let j = stack.length - commonSteps - 1; j >= 1; j--) {
let children = [];
levelChildren.push({
id: null,
isStateEditable: false,
name: parseCustomHookName(stack[j - 1].functionName),
value: undefined,
subHooks: children,
Expand All @@ -412,8 +417,22 @@ function buildTree(rootStack, readHookLog): HooksTree {
}
prevStack = stack;
}
const {primitive} = hook;

// For now, the "id" of stateful hooks is just the stateful hook index.
// Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
const id =
primitive === 'Context' || primitive === 'DebugValue'
? null
: nativeHookID++;

// For the time being, only State and Reducer hooks support runtime overrides.
const isStateEditable = primitive === 'Reducer' || primitive === 'State';

levelChildren.push({
name: hook.primitive,
id,
isStateEditable,
name: primitive,
value: hook.value,
subHooks: [],
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* 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.
*
* @emails react-core
* @jest-environment node
*/

'use strict';

describe('React hooks DevTools integration', () => {
let React;
let ReactDebugTools;
let ReactTestRenderer;
let act;
let overrideHookState;

beforeEach(() => {
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
inject: injected => {
overrideHookState = injected.overrideHookState;
},
supportsFiber: true,
onCommitFiberRoot: () => {},
onCommitFiberUnmount: () => {},
};

jest.resetModules();

React = require('react');
ReactDebugTools = require('react-debug-tools');
ReactTestRenderer = require('react-test-renderer');

act = ReactTestRenderer.act;
});

it('should support editing useState hooks', () => {
let setCountFn;

function MyComponent() {
const [count, setCount] = React.useState(0);
setCountFn = setCount;
return <div>count:{count}</div>;
}

const renderer = ReactTestRenderer.create(<MyComponent />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['count:', '0'],
});

const fiber = renderer.root.findByType(MyComponent)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
const stateHook = tree[0];
expect(stateHook.isStateEditable).toBe(true);

if (__DEV__) {
overrideHookState(fiber, stateHook.id, [], 10);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['count:', '10'],
});

act(() => setCountFn(count => count + 1));
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['count:', '11'],
});
}
});

it('should support editable useReducer hooks', () => {
const initialData = {foo: 'abc', bar: 123};

function reducer(state, action) {
switch (action.type) {
case 'swap':
return {foo: state.bar, bar: state.foo};
default:
throw new Error();
}
}

let dispatchFn;
function MyComponent() {
const [state, dispatch] = React.useReducer(reducer, initialData);
dispatchFn = dispatch;
return (
<div>
foo:{state.foo}, bar:{state.bar}
</div>
);
}

const renderer = ReactTestRenderer.create(<MyComponent />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['foo:', 'abc', ', bar:', '123'],
});

const fiber = renderer.root.findByType(MyComponent)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
const reducerHook = tree[0];
expect(reducerHook.isStateEditable).toBe(true);

if (__DEV__) {
overrideHookState(fiber, reducerHook.id, ['foo'], 'def');
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['foo:', 'def', ', bar:', '123'],
});

act(() => dispatchFn({type: 'swap'}));
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['foo:', '123', ', bar:', 'def'],
});
}
});

// This test case is based on an open source bug report:
// facebookincubator/redux-react-hook/issues/34#issuecomment-466693787
it('should handle interleaved stateful hooks (e.g. useState) and non-stateful hooks (e.g. useContext)', () => {
const MyContext = React.createContext(1);

let setStateFn;
function useCustomHook() {
const context = React.useContext(MyContext);
const [state, setState] = React.useState({count: context});
React.useDebugValue(state.count);
setStateFn = setState;
return state.count;
}

function MyComponent() {
const count = useCustomHook();
return <div>count:{count}</div>;
}

const renderer = ReactTestRenderer.create(<MyComponent />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['count:', '1'],
});

const fiber = renderer.root.findByType(MyComponent)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
const stateHook = tree[0].subHooks[1];
expect(stateHook.isStateEditable).toBe(true);

if (__DEV__) {
overrideHookState(fiber, stateHook.id, ['count'], 10);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['count:', '10'],
});

act(() => setStateFn(state => ({count: state.count + 1})));
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['count:', '11'],
});
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ describe('ReactHooksInspection', () => {
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: true,
id: 0,
name: 'State',
value: 'hello world',
subHooks: [],
Expand All @@ -48,10 +50,14 @@ describe('ReactHooksInspection', () => {
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Custom',
value: __DEV__ ? 'custom hook label' : undefined,
subHooks: [
{
isStateEditable: true,
id: 0,
name: 'State',
value: 'hello world',
subHooks: [],
Expand Down Expand Up @@ -80,31 +86,43 @@ describe('ReactHooksInspection', () => {
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Custom',
value: undefined,
subHooks: [
{
isStateEditable: true,
id: 0,
name: 'State',
subHooks: [],
value: 'hello',
},
{
isStateEditable: false,
id: 1,
name: 'Effect',
subHooks: [],
value: effect,
},
],
},
{
isStateEditable: false,
id: null,
name: 'Custom',
value: undefined,
subHooks: [
{
isStateEditable: true,
id: 2,
name: 'State',
value: 'world',
subHooks: [],
},
{
isStateEditable: false,
id: 3,
name: 'Effect',
value: effect,
subHooks: [],
Expand Down Expand Up @@ -143,50 +161,70 @@ describe('ReactHooksInspection', () => {
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Bar',
value: undefined,
subHooks: [
{
isStateEditable: false,
id: null,
name: 'Custom',
value: undefined,
subHooks: [
{
isStateEditable: true,
id: 0,
name: 'Reducer',
value: 'hello',
subHooks: [],
},
{
isStateEditable: false,
id: 1,
name: 'Effect',
value: effect,
subHooks: [],
},
],
},
{
isStateEditable: false,
id: 2,
name: 'LayoutEffect',
value: effect,
subHooks: [],
},
],
},
{
isStateEditable: false,
id: null,
name: 'Baz',
value: undefined,
subHooks: [
{
isStateEditable: false,
id: 3,
name: 'LayoutEffect',
value: effect,
subHooks: [],
},
{
isStateEditable: false,
id: null,
name: 'Custom',
subHooks: [
{
isStateEditable: true,
id: 4,
name: 'Reducer',
subHooks: [],
value: 'world',
},
{
isStateEditable: false,
id: 5,
name: 'Effect',
subHooks: [],
value: effect,
Expand All @@ -208,6 +246,8 @@ describe('ReactHooksInspection', () => {
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Context',
value: 'default',
subHooks: [],
Expand Down Expand Up @@ -270,9 +310,19 @@ describe('ReactHooksInspection', () => {
let tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Custom',
value: __DEV__ ? 'bar:123' : undefined,
subHooks: [{name: 'State', subHooks: [], value: 0}],
subHooks: [
{
isStateEditable: true,
id: 0,
name: 'State',
subHooks: [],
value: 0,
},
],
},
]);
});
Expand Down
Loading