Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/hooks/useEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
/* eslint-disable react-hooks/exhaustive-deps */
import * as React from 'react';

function useEvent<T extends Function>(callback: T): T {
const fnRef = React.useRef<any>();
function useEvent<T extends ((...args: any[]) => any) | undefined>(
callback: T,
): undefined extends T
? (
...args: Parameters<NonNullable<T>>
) => ReturnType<NonNullable<T>> | undefined
: T {
const fnRef = React.useRef<T | undefined>(callback);
fnRef.current = callback;

const memoFn = React.useCallback<T>(
((...args: any) => fnRef.current?.(...args)) as any,
const memoFn = React.useCallback(
(...args: any[]) => fnRef.current?.(...args),
[],
);

Expand Down
20 changes: 20 additions & 0 deletions tests/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useMobile from '../src/hooks/useMobile';
import useState from '../src/hooks/useState';
import useSyncState from '../src/hooks/useSyncState';
import useControlledState from '../src/hooks/useControlledState';
import useEvent from '../src/hooks/useEvent';

global.disableUseId = false;

Expand Down Expand Up @@ -706,4 +707,23 @@ describe('hooks', () => {
expect(container.textContent).toEqual('2');
});
});

describe('useEvent', () => {
it('extract type', () => {
const Demo = (props: {
canUndefined?: (a: number) => boolean;
notUndefined: (a: number) => boolean;
}) => {
const ua = useEvent(props.canUndefined);
const ub = useEvent(props.notUndefined);

ua(1);
ub(2);

return null;
};

expect(Demo).toBeTruthy();
});
});
Comment on lines +711 to +728

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这个测试用例目前只在编译时检查类型,通过 expect(Demo).toBeTruthy() 来断言组件定义本身。这并不能验证 useEvent hook 的运行时行为。

建议增加更全面的测试,覆盖以下场景:

  1. 运行时行为:渲染组件,并实际调用 useEvent 返回的函数,断言回调函数是否被正确调用。
  2. 回调更新:测试当 callback prop 变更后,useEvent 返回的函数是否会调用最新的回调。
  3. undefined 场景:测试当传入 undefined 时,调用返回的函数不会抛出错误。

一个更完善的测试可能如下所示:

it('should call the latest function', () => {
  const fn1 = jest.fn();
  const fn2 = jest.fn();

  const Test = ({ cb }: { cb: () => void }) => {
    const eventFn = useEvent(cb);
    return <button onClick={eventFn}>Click</button>;
  };

  const { getByText, rerender } = render(<Test cb={fn1} />);
  fireEvent.click(getByText('Click'));
  expect(fn1).toHaveBeenCalledTimes(1);
  expect(fn2).toHaveBeenCalledTimes(0);

  rerender(<Test cb={fn2} />);
  fireEvent.click(getByText('Click'));
  expect(fn1).toHaveBeenCalledTimes(1);
  expect(fn2).toHaveBeenCalledTimes(1);
});

it('should not throw error when callback is undefined', () => {
  const Test = ({ cb }: { cb?: () => void }) => {
    const eventFn = useEvent(cb);
    return <button onClick={eventFn}>Click</button>;
  };

  const { getByText } = render(<Test cb={undefined} />);
  expect(() => {
    fireEvent.click(getByText('Click'));
  }).not.toThrow();
});

});
Loading