testing-library/react-hooks-testing-library

renderHook() does not clean useRef in custom hooks

flora8984461 opened this issue · 2 comments

  • react-hooks-testing-library version: 7.0.2
  • react version: 16.14
  • react-dom version (if applicable): 16.14
  • react-test-renderer version (if applicable):
  • node version: 16.16
  • npm (or yarn) 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;

image

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:

reproduce-jest-useref.zip

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.