Sylvain Melisson
Sylvain Melisson

Reputation: 43

React testing hook with callback

I'm trying to test a simple hook i've made for intercepting offline/online events:

import { useEffect } from 'react';

const useOfflineDetection = (
  setOffline: (isOffline: boolean) => void
): void => {
  useEffect(() => {
    window.addEventListener('offline', () => setOffline(true));
    window.addEventListener('online', () => setOffline(false));

    return () => {
      window.removeEventListener('offline', () => setOffline(true));
      window.removeEventListener('online', () => setOffline(false));
    };
  }, []);
};

export default useOfflineDetection;

------------------------------------

//...somewhere else in the code

useOfflineDetection((isOffline: boolean) => Do something with 'isOffline');

But I'm not sure I'm using the correct way to return value and moreover I'm not sure to get how to test it with jest, @testing-library & @testing-library/react-hooks.

I missunderstand how to mount my hook and then catch the return provide by callback.

Is someone can help me ? I'm stuck with it :'(

Thanks in advance!

EDIT:

Like Estus Flask said, I can use useEffect instead callback like I design it first.

import { useEffect, useState } from 'react';

const useOfflineDetection = (): boolean => {
  const [isOffline, setIsOffline] = useState<boolean>(false);
  useEffect(() => {
    window.addEventListener('offline', () => setIsOffline(true));
    window.addEventListener('online', () => setIsOffline(false));

    return () => {
      window.removeEventListener('offline', () => setIsOffline(true));
      window.removeEventListener('online', () => setIsOffline(false));
    };
  }, []);

  return isOffline;
};

export default useOfflineDetection;

------------------------------------

//...somewhere else in the code

const isOffline = useOfflineDetection();

Do something with 'isOffline'

But if I want to use this hook in order to store "isOffline" with something like redux or other, the only pattern I see it's using useEffect:

const isOffline = useOfflineDetection();
useEffect(() => {
   dispatch(setIsOffline(isOffline));
}, [isOffline])

instead of just:

useOfflineDetection(isOffline => dispatch(setIsOffline(isOffline)));

But is it that bad ?

Upvotes: 1

Views: 1645

Answers (1)

Estus Flask
Estus Flask

Reputation: 223288

The problem with the hook is that clean up will fail because addEventListener and removeEventListener callbacks are different. They should be provided with the same functions:

const setOfflineTrue = useCallback(() => setOffline(true), []);
const setOfflineFalse = useCallback(() => setOffline(false), []);

useEffect(() => {
  window.addEventListener('offline', setOfflineTrue);
  ...

Then React Hooks Testing Library can be used to test a hook.

Since DOM event targets have determined behaviour that is supported by Jest DOM to some extent, respective events can be dispatched to test a callback:

const mockSetOffline = jest.fn();
const wrapper = renderHook(() => useOfflineDetection(mockSetOffline));

expect(mockSetOffline).not.toBeCalled();

// called only on events
window.dispatchEvent(new Event('offline'));
expect(mockSetOffline).toBeCalledTimes(1);
expect(mockSetOffline).lastCalledWith(false);

window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(2);
expect(mockSetOffline).lastCalledWith(true);

// listener is registered once
wrapper.rerender();
expect(mockSetOffline).toBeCalledTimes(2);

window.dispatchEvent(new Event('offline'));
expect(mockSetOffline).toBeCalledTimes(3);
expect(mockSetOffline).lastCalledWith(false);

window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(4);
expect(mockSetOffline).lastCalledWith(true);

// cleanup is done correctly
window.dispatchEvent(new Event('offline'));
window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(4);

Upvotes: 2

Related Questions