0

Test set state is not called when component is unmounted

I have got a custom hook which calls a promise on page load to set the data. To make sure that set state on data/error is not called when if the the component unmount I am using cancel request as below:


function useService() {
    const [data, setData] = useState([]);
    const [error, setError] = useState("");
    const [loading, setLoading] = useState({});
    const [cancelRequest, setCancelRequest] = useState(false);

    const getMessgaes = async () => {
        setLoading(true);
        try {
            const res = await getChatLog();
            if (!cancelRequest) {
                setData(res);
                setLoading(false);
            }
        } catch (e) {
            if (!cancelRequest) {
                setError(e);
                setLoading(false);
            }
        }

    };

    useEffect(() => {
        getMessgaes();
        return () => {
            setCancelRequest(true);
        };
    }, []);


    return {
        data, error, loading, cancelRequest
    }
}

My test are:

   it("if component is unmounted before response then set data is not called", async () => {
        getChatLog.mockImplementation(() => {
            setTimeout(()=>{
                Promise.reject("error");
            },500);
        });
        const {result, unmount, waitForNextUpdate} = renderHook(() => useService());
        expect(result.current.cancelRequest).toEqual(false);
        unmount();
        expect(result.current.cancelRequest).toEqual(true);
        await waitForNextUpdate();
        expect(getChatLog).toHaveBeenCalledTimes(3);
        expect(result.current.error).toEqual("");
    });

but I am getting error:


    Warning: An update to TestHook inside a test was not wrapped in act(...).

    When testing, code that causes React state updates should be wrapped into act(...):

    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */


    Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
        in TestHook
        in Suspense

Can someone please guide how this can be tested?

Thanks

Lin Du
  • 88,126
  • 95
  • 281
  • 483
reactdesign
  • 167
  • 1
  • 11

1 Answers1

0

First of all, never call setState() in clean up function of useEffect hook, same as componentWillUnmount().

You should not call setState() in componentWillUnmount() because the component will never be re-rendered. Once a component instance is unmounted, it will never be mounted again.

So your clean-up of useEffect code under test doesn't make sense.

Secondly, using setTimeout() with a delay for a mock implementation doesn't make sense either, we don't need to delay it in test cases, it makes the test case slowly and we have to mock the timer for it. You can use mock.mockRejectedValueOnce('error') instead.

At last, you should await waitForNextUpdate() before the assertion to solve the warning: Warning: An update to TestComponent inside a test was not wrapped in act(...)..

E.g.

useService.ts:

import { useEffect, useState } from 'react';
import { getChatLog } from './getChatLog';

export function useService() {
  const [data, setData] = useState<string[]>([]);
  const [error, setError] = useState('');
  const [loading, setLoading] = useState({});
  const [cancelRequest, setCancelRequest] = useState(false);

  const getMessgaes = async () => {
    setLoading(true);
    try {
      const res = await getChatLog();
      if (!cancelRequest) {
        setData(res);
        setLoading(false);
      }
    } catch (e) {
      if (!cancelRequest) {
        setError(e);
        setLoading(false);
      }
    }
  };

  useEffect(() => {
    getMessgaes();
  }, []);

  return { data, error, loading, cancelRequest };
}

getChatLog.ts:

export async function getChatLog() {
  return ['real data'];
}

useService.test.ts:

import { renderHook } from '@testing-library/react-hooks';
import { useService } from './useService';
import { getChatLog } from './getChatLog';
import { mocked } from 'ts-jest/utils';

jest.mock('./getChatLog');

describe('58026328', () => {
  test('should pass', async () => {
    const mGetChatLog = mocked(getChatLog);
    mGetChatLog.mockRejectedValueOnce('error');
    const { result, waitForNextUpdate } = renderHook(useService);
    await waitForNextUpdate();
    expect(result.current.cancelRequest).toEqual(false);
  });
});

Test result:

 PASS  examples/58026328/useService.test.ts
  58026328
    ✓ should pass (20 ms)

---------------|---------|----------|---------|---------|-------------------
File           | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
---------------|---------|----------|---------|---------|-------------------
All files      |   82.61 |       25 |      80 |   81.82 |                   
 getChatLog.ts |      50 |      100 |       0 |      50 | 2                 
 useService.ts |   85.71 |       25 |     100 |      85 | 14-16             
---------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.56 s, estimated 13 s
Lin Du
  • 88,126
  • 95
  • 281
  • 483