enzymejs / enzymejs/enzyme

New hooks api suggestion / discussion -- inspectHook() and setHookState()

Open
#2,120 5 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
19.8k
Forks
2k
PR merge metrics
No merged PRs in 30d

Description

With current enzyme api, usually we have to get / set hook state from rendered output (e.g. print state in rendered `div` / `setCount` on button). it's hard to check and change current hook state value through enzyme API (like `.state()` and `.setState()` for class component.). Though some (or lots of ?) people may think it's testing implementation detail, I think it's still valuable to have an api to get / set the result of hook state.

There are two new apis I'd like to suggest. Hope to get some more feedbacks :-)

## `inspectHook()`

The first is `inspectHook()` -- an api to get the whole hook stack tree in current function component node. In fact `inspectHookWithFiber()` is an api name from the [react-debug-tool](https://github.com/facebook/react/blob/master/packages/react-debug-tools/src/ReactDebugHooks.js) in React's internal packages for hook testing, and I'd like to port this module into enzyme.

With `inspectHookWithFiber()` then we can get the whole hook stack tree in a render function and write test like [this](https://github.com/facebook/react/blob/master/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js#L43). The tree currently has [the following structure](https://github.com/facebook/react/blob/master/packages/react-debug-tools/src/ReactDebugHooks.js#L234)

```js
// from source code of above link
type HooksNode = {
id: number | null,
isStateEditable: boolean,
name: string,
value: mixed,
subHooks: Array,
};
```

we can get the state from `value`, hook function name from `name`, and subHooks (sub hooks called in this hook) in `subHooks`.

In fact in react-devtool [they also get current hook state value with it after some tweaks](https://github.com/facebook/react-devtools/blob/master/backend/ReactDebugHooks.js#L21).

### use cases

Basically the use cases would be similar to the tests in `react-debug-tools`. The following use case is an example of `inspectHook()` in enzyme; For such simple test case the better way to test the counter may be just `find()` the `

` and assert the count, though. :

```js

function Counter() {
const [count, setCount] = React.useState(0);
const increment = () => setCount(prevCount => prevCount + 1);;
return (



{count}



);
}

const wrapper = mount();
const tree = wrapper.inspectHook();
expect(tree[0].value).toEqual(0);
ReactTestUtils.act(() => wrapper.find('button').prop('onClick')());
const nextTree = wrapper.inspectHook();
expect(nextTree[0].value).toEqual(1);

```

Note that not only `useState()` will be in the tree returned from `inspectHook()`; All hooks called in the function component will be logged, if it's a hook with state the `value` will be the state, or it would depends on what the hook is.

### limitation

With this way we can get the state of hook, but we cannot get the returned value of hook calls. So it's still not possible to get the `seState` returned from `useState()`, or `dispatch()` from `useReducer()`, or returned value from custom hook in this way.

Also I'm not sure if it's doable in `shallow()` since shallow renderer is not based on fiber.

### implementation

1. given a wrapper of single functional component node, find its relative fiber node.
2. pass it into `inspectHookOfFiber()` which we bring into enzyme and return the result.

As for porting `inspectHookOfFiber()`, we have to [get the hook dispatcher in react secret shared internals in enzyme](https://github.com/facebook/react/blob/master/packages/react-debug-tools/src/ReactDebugHooks.js#L573). In react-devtool they have their own way to communicate with React (React in fact [*inject* the secret internal into the react-devtool](https://github.com/facebook/react/blob/master/packages/react-dom/src/client/ReactDOM.js#L860) to make it work). I'm sure we can get the secret internal through [`React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`](https://github.com/facebook/react/blob/master/packages/shared/ReactSharedInternals.js).
Though it works to use it directly, It's not a good idea to couple with that private internal. The alternative way may be add [devtool hook](https://github.com/facebook/react-devtools/blob/master/backend/installGlobalHook.js) in enzyme side and let the React inject the internal into enzyme. I'll try to look into this further more.

## `setHookState()`

The second is `setHookState()` -- an api to set the state of hook and trigger the rerender. Like `inspectHook()` there's also an internal function in React called [`overrideHookState`](https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberReconciler.js#L380), which is injected into react-devtools [here](https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberReconciler.js#L437) so you can edit the state value of hook in react-devtool. I'd also like to port this functionality into enzyme, too.

### use case

The usage of `overrideHookState` in React is like [this](https://github.com/facebook/react/blob/master/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js#L119)

```js
function Counter() {
const [count, setCount] = React.useState(0);
const increment = () => setCount(prevCount => prevCount + 1);;
return (



{count}



);
}

const renderer = ReactTestRenderer.create();
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);

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

In enzyme we could find the fiber and handle the state update path (the 3rd arg) by ourselves. So the api only need to receive 2 arguments: `id` for the hook id, which is included in the tree node returned from `inspectHook`, and `value` for the changed value:

```js

function Counter() {
const [count, setCount] = React.useState(0);
const increment = () => setCount(prevCount => prevCount + 1);;
return (



{count}



);
}

const wrapper = mount();
const tree = wrapper.inspectHook();
const stateHook = tree[0];
expect(stateHook.value).toBe(0);

wrapper.setHookState(stateHook.id, 10);

expect(wrapper.find('p').text()).toEqual('10');

```

### implementation

To port the `overrideHookState` it looks like the only way is to let React inject devtool internal into it. I think we could build a same global hook as [react-devtool](https://github.com/facebook/react-devtools/blob/master/backend/installGlobalHook.js) before import React, but I haven't tried it yet nor known if we can always ensure React injects internal after we install the hook.

If it's doable then the remaining work is as same as `inspectHook` -- find the fiber node and call `overrideHookState` to get the result.

## Planning

If these sound good, I'd like to work on this with following steps:

1. Port `inspectHookOfFiber` into enzyme, with hard-coding secret internal (i.e. directly use `React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`), and prove that we are able to add `.inspectHook()` in enzyme.
2. Try to add a [devtool hook](https://github.com/facebook/react-devtools/blob/master/backend/installGlobalHook.js) in enzyme so we don't need the hard coding internal.
3. if (2) works then building `setHookState()` should be relatively easy.

Also note that https://github.com/facebook/react/pull/14906#issue-254828738 said that `react-debug-tools` should be released soon. Once it's released we can just use `inspectHookOfFiber()` without porting (But might still need to build a devtool hook for `overrideHookState`). I'll also open an issue to request the releasing if new apis are desirable. It also might be good to start work after React releasing this.

And I'm not sure if it's what enzyme maintainers / users want. So feedback are welcome :-)

Related old discussion / PR in React / react-devtool: https://github.com/facebook/react-devtools/pull/1272 https://github.com/facebook/react/pull/14906

Contributor guide

Open the contributing guide

Research direction

Start by reading ReactDebugHooks.js and ReactFiberReconciler.js, especially inspectHooksOfFiber and overrideHookState, then review the React debug-tools integration examples linked in the issue. Determine whether Enzyme can locate a functional component's fiber and obtain the required React internals. Done means a maintainer-approved design and tested inspectHook() and setHookState() APIs, including their mount and shallow-renderer limitations.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, react
Domain
frontend, testing
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.