renderHook() does not clean useRef in custom hooks
flora8984461 opened this issue · 2 comments
react-hooks-testing-library
version: 7.0.2react
version: 16.14react-dom
version (if applicable): 16.14react-test-renderer
version (if applicable):node
version: 16.16npm
(oryarn
) version: 1.22
Relevant code or config:
I have a custom hook:
const initialState = { x: null, y: null };
const useCustomHook = (elementRef) => {
const touchState = useRef(initialState);
useEffect(() => {
const element = elementRef?.current;
const touchStartHandler = (e) => {
// add this log when test is running
console.log(touchState.current);
touchState.current.y = e.targetTouches[0].clientY
}
const touchMoveHandler= (e) => {
touchState.current.y = e.targetTouches[0].clientY
}
const touchEndHandler= () => {
touchState.current.y = null;
}
element.addEventListener("touchstart", touchStartHandler);
element.addEventListener("touchmove", touchMoveHandler);
element.addEventListener("touchend", touchEndHandler);
return () => {
element.removeEventListener("touchstart", touchStartHandler);
element.removeEventListener("touchmove", touchMoveHandler);
element.removeEventListener("touchend", touchEndHandler);
};
}, [elementRef])
}
And in my test files:
import { renderHook, cleanup } from '@testing-library/react-hooks';
const getMockTouch = (target, clientY = 50) => ({
clientY,
clientX: 50,
target,
force: 1,
identifier: 1,
pageX: 100,
pageY: 100,
radiusX: 1,
radiusY: 1,
rotationAngle: 0,
screenX: 100,
screenY: 100,
});
describe('useCustomHook', () => {
afterEach(async () => {
jest.clearAllMocks();
await cleanup();
});
afterAll(() => {
jest.resetModules();
});
it('test 1', async () => {
const div = document.createElement('div');
document.body.append(div);
renderHook(() =>
useCustomHook({current: div})
);
div.dispatchEvent(
new TouchEvent('touchstart', {
targetTouches: [getMockTouch(div, 50)],
})
);
div.dispatchEvent(
new TouchEvent('touchmove', {
targetTouches: [getMockTouch(div, 55)],
})
);
div.dispatchEvent(
new TouchEvent('touchmove', {
targetTouches: [getMockTouch(div, 65)],
})
);
expect(something...);
});
it('test 2', async () => {
const div = document.createElement('div');
document.body.append(div);
renderHook(() =>
useCustomHook({current: div})
);
//// I log the touchState, and in this test 2, touchState is not the initial value, but the same as changed in test1.
div.dispatchEvent(
new TouchEvent('touchstart', {
targetTouches: [getMockTouch(div, 50)],
})
);
});
})
What you did:
As the code above shows, I have a custom useHook, and I useRef() to create initial state and modify the state as the touch moves;
Then I create a test file for the custom useHook, I add cleanup after each test, and in every test, I render a new renderHook();
What happened:
In test 2, the initial touchState.current.y
value is 65 (the last set value in test1);
Expectation: it should be null because that's the initial value; test 1's ref value should not persist in the whole test;
In the screenshot, the red part, y should be null
Reproduction:
I attached a zip, once downloaded, run yarn
and then yarn test
.
Problem description:
In test 2, the initial touchState.current.y
value is 65 (the last set value in test1);
Expectation: it should be null because that's the initial value; test 1's ref value should not persist in the whole test;
Suggested solution:
Thank you for any help!
It looks to me like the issue here is that your hook will always use the same initialState
reference:
// this is never recreated and changes to `y` mutate this reference
// e.g. touchState.current.y = e.targetTouches[0].clientY
const initialState = { x: null, y: null };
const useCustomHook = (elementRef) => {
// always uses the const reference of initialState
const touchState = useRef(initialState);
// ...
}
Try, moving the initialState into the hook or just passing the object straight to useRef
if the variable is not needed:
const useCustomHook = (elementRef) => {
const initialState = { x: null, y: null };
const touchState = useRef(initialState);
// or
// const touchState = useRef({ x: null, y: null });
// ...
}
Even though the object is created each time the hook is call useRef
will always return the reference used in the first render of the hook. Now you should get a new reference for each renderHook
call you make and the values should not bleed between your tests anymore.
Thank you @mpeyper ! Moving the initialState into the useCustomHook function works. I think this issue can be closed then.