Skip to content

Commit

Permalink
refactor: implement custom useEvent hook
Browse files Browse the repository at this point in the history
  • Loading branch information
gkuzin13 committed Jan 1, 2024
1 parent fccfbb3 commit 826f539
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useRef } from 'react';
import { Layer } from 'react-konva';
import useEvent from '@/hooks/useEvent';
import useEvent from '@/hooks/useEvent/useEvent';
import { getUnregisteredPointerPosition } from './helpers/stage';
import { safeJSONParse } from '@/utils/object';
import { duplicateNodesAtPosition } from '@/utils/node';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
getNodesMinMaxPoints,
} from '@/utils/position';
import useForceUpdate from '@/hooks/useForceUpdate/useForceUpdate';
import useEvent from '@/hooks/useEvent';
import useEvent from '@/hooks/useEvent/useEvent';
import * as Styled from './ShapesThumbnail.styled';
import type { NodeObject, ThemeColorValue } from 'shared';
import type Konva from 'konva';
Expand Down
26 changes: 12 additions & 14 deletions apps/client/src/contexts/theme.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import useEvent from '@/hooks/useEvent/useEvent';
import { LOCAL_STORAGE_THEME_KEY } from '@/constants/app';
import { storage } from '@/utils/storage';
import { createContext } from './createContext';
Expand Down Expand Up @@ -30,23 +31,20 @@ export const ThemeProvider = ({ children }: React.PropsWithChildren) => {
}
}, []);

useEffect(() => {
if (storage.get<ThemeValue>(LOCAL_STORAGE_THEME_KEY)) {
return;
}
const darkColorScheme = useMemo(() => prefersDarkColorScheme(), []);

const darkColorScheme = prefersDarkColorScheme();
const handleColorSchemeChange = useCallback(
(event: MediaQueryListEvent) => {
if (storage.get<ThemeValue>(LOCAL_STORAGE_THEME_KEY)) {
return;
}

const handleColorSchemeChange = (event: MediaQueryListEvent) => {
handleThemeChange(event.matches ? 'dark' : 'default', false);
};

darkColorScheme.addEventListener('change', handleColorSchemeChange);
},
[handleThemeChange],
);

return () => {
darkColorScheme.removeEventListener('change', handleColorSchemeChange);
};
}, [handleThemeChange]);
useEvent('change', handleColorSchemeChange, darkColorScheme);

return (
<ThemeContext.Provider value={{ value: theme, set: handleThemeChange }}>
Expand Down
34 changes: 0 additions & 34 deletions apps/client/src/hooks/useEvent.ts

This file was deleted.

109 changes: 109 additions & 0 deletions apps/client/src/hooks/useEvent/useEvent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { fireEvent, renderHook } from '@testing-library/react';
import useEvent from './useEvent';

describe('useEvent', () => {
afterEach(() => {
vi.resetAllMocks();
});

it('calls the event listener when the event is triggered', async () => {
const element = document.createElement('button');
const handler = vi.fn();

renderHook(() => useEvent('click', handler, element));

fireEvent.click(element);

expect(handler).toHaveBeenCalledTimes(1);
});

it('should bind/unbind the event listener to the window object if no element is provided', async () => {
const spyAddEventListener = vi.spyOn(window, 'addEventListener');
const spyRemoveEventListener = vi.spyOn(window, 'removeEventListener');

const handler = vi.fn();
const options = undefined;

const { unmount } = renderHook(() => useEvent('click', handler));

expect(spyAddEventListener).toHaveBeenCalledTimes(1);
expect(spyAddEventListener).toHaveBeenCalledWith('click', handler, options);

unmount();

expect(spyRemoveEventListener).toHaveBeenCalledTimes(1);
expect(spyRemoveEventListener).toHaveBeenCalledWith(
'click',
handler,
options,
);
});

it('should bind/unbind the event listener if element is provided', async () => {
const element = document.createElement('button');
const spyAddEventListener = vi.spyOn(element, 'addEventListener');
const spyRemoveEventListener = vi.spyOn(element, 'removeEventListener');

const handler = vi.fn();
const options = undefined;

const { unmount } = renderHook(() =>
useEvent('click', handler, element, options),
);

expect(spyAddEventListener).toHaveBeenCalledTimes(1);
expect(spyAddEventListener).toHaveBeenCalledWith('click', handler, options);

unmount();

expect(spyRemoveEventListener).toHaveBeenCalledTimes(1);
expect(spyRemoveEventListener).toHaveBeenCalledWith(
'click',
handler,
options,
);
});

it('should not bind the event listener if handler is undefined', async () => {
const element = document.createElement('button');
const spyAddEventListener = vi.spyOn(element, 'addEventListener');
const spyRemoveEventListener = vi.spyOn(element, 'removeEventListener');

const handler = undefined;

const { unmount } = renderHook(() => useEvent('click', handler, element));

expect(spyAddEventListener).toHaveBeenCalledTimes(0);

unmount();

expect(spyRemoveEventListener).toHaveBeenCalledTimes(0);
});

it('should pass options to the event listener', async () => {
const element = document.createElement('button');
const spyAddEventListener = vi.spyOn(element, 'addEventListener');
const spyRemoveEventListener = vi.spyOn(element, 'removeEventListener');

const handler = vi.fn();
const options = {
capture: true,
passive: true,
once: true,
};

const { unmount } = renderHook(() =>
useEvent('click', handler, element, { eventOptions: options }),
);

expect(spyAddEventListener).toHaveBeenCalledWith('click', handler, options);

unmount();

expect(spyRemoveEventListener).toHaveBeenCalledWith(
'click',
handler,
options,
);
});
});
48 changes: 48 additions & 0 deletions apps/client/src/hooks/useEvent/useEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { isBrowser } from '@/utils/is';
import { useEffect, useRef } from 'react';

type EventsMap = HTMLElementEventMap &
WindowEventMap &
DocumentEventMap &
MediaQueryListEventMap &
WebSocketEventMap;

type HandlerElements =
| Document
| HTMLElement
| MediaQueryList
| Window
| WebSocket;

const defaultTarget = isBrowser ? window : null;

function useEvent<K extends keyof EventsMap>(
name: K,
handler: ((event: EventsMap[K]) => void) | undefined,
element: HandlerElements | null = defaultTarget,
options?: {
eventOptions?: AddEventListenerOptions;
deps?: unknown[];
},
) {
const savedHandler = useRef(handler);

useEffect(() => {
savedHandler.current = handler;
}, [handler]);

useEffect(() => {
if (!element || !savedHandler.current) return;

const listener = savedHandler.current as EventListenerOrEventListenerObject;
const eventOptions = options?.eventOptions;

element.addEventListener(name, listener, eventOptions);

return () => {
element.removeEventListener(name, listener, eventOptions);
};
}, [name, element, JSON.stringify(options)]);
}

export default useEvent;
22 changes: 9 additions & 13 deletions apps/client/src/hooks/useNetworkState/useNetworkState.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import useEvent from '@/hooks/useEvent/useEvent';

type NetworkState = {
online?: boolean;
};

const isNavigator = typeof navigator !== 'undefined';

const eventOptions = { passive: true };

function getConnectionState(): NetworkState {
return {
online: isNavigator ? navigator.onLine : undefined,
Expand All @@ -15,19 +18,12 @@ function getConnectionState(): NetworkState {
function useNetworkState() {
const [state, setState] = useState(getConnectionState());

useEffect(() => {
const handleStateChange = () => {
setState(getConnectionState());
};

window.addEventListener('online', handleStateChange, { passive: true });
window.addEventListener('offline', handleStateChange, { passive: true });
const handleStateChange = useCallback(() => {
setState(getConnectionState());
}, []);

return () => {
window.removeEventListener('online', handleStateChange);
window.removeEventListener('offline', handleStateChange);
};
});
useEvent('online', handleStateChange, window, { eventOptions });
useEvent('offline', handleStateChange, window, { eventOptions });

return state;
}
Expand Down

0 comments on commit 826f539

Please sign in to comment.