New hooks api suggestion / discussion -- inspectHook() and setHookState()
- 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
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