Merge pull request #595 from macinjoke/useEvent-type-inference

improve useEvent type
This commit is contained in:
Vadim Dalecky 2019-11-05 23:21:13 -05:00 committed by GitHub
commit 7154ca8d71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 144 additions and 13 deletions

View File

@ -0,0 +1,118 @@
import { renderHook } from '@testing-library/react-hooks';
import useEvent, { ListenerType1, ListenerType2 } from '../useEvent';
interface Props {
name: string;
handler: (...args: any[]) => void;
target: ListenerType1 | ListenerType2;
options: any;
}
const propsList1 = [
{
name: 'name1',
handler: () => {},
target: {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
},
options: { a: 'opt1' },
},
{
name: 'name2',
handler: () => {},
target: {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
},
options: { a: 'opt2' },
},
];
const propsList2 = [
{
...propsList1[0],
target: {
on: jest.fn(),
off: jest.fn(),
},
},
{
...propsList1[1],
target: {
on: jest.fn(),
off: jest.fn(),
},
},
];
it('should call addEventListener/removeEventListener on mount/unmount', () => {
checkOnMountAndUnmount(propsList1[0], 'addEventListener', 'removeEventListener');
});
it('should call on/off on mount/unmount', () => {
checkOnMountAndUnmount(propsList2[0], 'on', 'off');
});
it('should call addEventListener/removeEventListener on deps changes', () => {
checkOnDepsChanges(propsList1[0], propsList1[1], 'addEventListener', 'removeEventListener');
});
it('should call on/off on deps changes', () => {
checkOnDepsChanges(propsList2[0], propsList2[1], 'on', 'off');
});
const checkOnMountAndUnmount = (props: Props, addEventListenerName: string, removeEventListenerName: string) => {
const { unmount } = renderHook((p: Props) => useEvent(p.name, p.handler, p.target, p.options), {
initialProps: props,
});
expect(props.target[addEventListenerName]).toHaveBeenCalledTimes(1);
expect(props.target[addEventListenerName]).toHaveBeenLastCalledWith(props.name, props.handler, props.options);
unmount();
expect(props.target[removeEventListenerName]).toHaveBeenCalledTimes(1);
expect(props.target[removeEventListenerName]).toHaveBeenLastCalledWith(props.name, props.handler, props.options);
};
const checkOnDepsChanges = (
props1: Props,
props2: Props,
addEventListenerName: string,
removeEventListenerName: string
) => {
const { rerender } = renderHook((p: Props) => useEvent(p.name, p.handler, p.target, p.options), {
initialProps: props1,
});
expect(props1.target[addEventListenerName]).toHaveBeenCalledTimes(1);
expect(props1.target[addEventListenerName]).toHaveBeenLastCalledWith(props1.name, props1.handler, props1.options);
// deps are same as previous
rerender({ name: props1.name, handler: props1.handler, target: props1.target, options: props1.options });
expect(props1.target[removeEventListenerName]).not.toHaveBeenCalled();
// name is different from previous
rerender({ name: props2.name, handler: props1.handler, target: props1.target, options: props1.options });
expect(props1.target[removeEventListenerName]).toHaveBeenCalledTimes(1);
expect(props1.target[removeEventListenerName]).toHaveBeenLastCalledWith(props1.name, props1.handler, props1.options);
// handler is different from previous
rerender({ name: props2.name, handler: props2.handler, target: props1.target, options: props1.options });
expect(props1.target[removeEventListenerName]).toHaveBeenCalledTimes(2);
expect(props1.target[removeEventListenerName]).toHaveBeenLastCalledWith(props2.name, props1.handler, props1.options);
// options contents is same as previous
rerender({ name: props2.name, handler: props2.handler, target: props1.target, options: { a: 'opt1' } });
expect(props1.target[removeEventListenerName]).toHaveBeenCalledTimes(2);
// options is different from previous
rerender({ name: props2.name, handler: props2.handler, target: props1.target, options: props2.options });
expect(props1.target[removeEventListenerName]).toHaveBeenCalledTimes(3);
expect(props1.target[removeEventListenerName]).toHaveBeenLastCalledWith(props2.name, props2.handler, props1.options);
// target is different from previous
rerender({ name: props2.name, handler: props2.handler, target: props2.target, options: props2.options });
expect(props1.target[removeEventListenerName]).toHaveBeenCalledTimes(4);
expect(props1.target[removeEventListenerName]).toHaveBeenLastCalledWith(props2.name, props2.handler, props2.options);
expect(props2.target[addEventListenerName]).toHaveBeenCalledTimes(1);
expect(props2.target[addEventListenerName]).toHaveBeenLastCalledWith(props2.name, props2.handler, props2.options);
};

View File

@ -3,25 +3,32 @@ import { isClient } from './util';
export interface ListenerType1 {
addEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
removeEventListener(name: string, handler: (event?: any) => void);
removeEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
}
export interface ListenerType2 {
on(name: string, handler: (event?: any) => void, ...args: any[]);
off(name: string, handler: (event?: any) => void);
off(name: string, handler: (event?: any) => void, ...args: any[]);
}
export type UseEventTarget = ListenerType1 | ListenerType2;
const defaultTarget = isClient ? window : null;
const useEvent = (
name: string,
handler?: null | undefined | ((event?: any) => void),
target: null | UseEventTarget = defaultTarget,
options?: any
const isListenerType1 = (target: any): target is ListenerType1 => {
return !!target.addEventListener;
};
const isListenerType2 = (target: any): target is ListenerType2 => {
return !!target.on;
};
type AddEventListener<T> = T extends ListenerType1 ? T['addEventListener'] : T extends ListenerType2 ? T['on'] : never;
const useEvent = <T extends UseEventTarget>(
name: Parameters<AddEventListener<T>>[0],
handler?: null | undefined | Parameters<AddEventListener<T>>[1],
target: null | T | Window = defaultTarget,
options?: Parameters<AddEventListener<T>>[2]
) => {
useEffect(() => {
if (!handler) {
@ -30,11 +37,17 @@ const useEvent = (
if (!target) {
return;
}
const fn: any = (target as ListenerType1).addEventListener || (target as ListenerType2).on;
fn.call(target, name, handler, options);
if (isListenerType1(target)) {
target.addEventListener(name, handler, options);
} else if (isListenerType2(target)) {
target.on(name, handler, options);
}
return () => {
const cleanFn: any = (target as ListenerType1).removeEventListener || (target as ListenerType2).off;
cleanFn.call(target, name, handler, options);
if (isListenerType1(target)) {
target.removeEventListener(name, handler, options);
} else if (isListenerType2(target)) {
target.off(name, handler, options);
}
};
}, [name, handler, target, JSON.stringify(options)]);
};