Skip to content

Commit

Permalink
feat: add useTimeoutFn
Browse files Browse the repository at this point in the history
[skip ci]
  • Loading branch information
streamich authored Aug 22, 2019
2 parents 255c600 + 210ea60 commit 284e6fd
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
- [`useInterval`](./docs/useInterval.md) — re-renders component on a set interval using `setInterval`.
- [`useSpring`](./docs/useSpring.md) — interpolates number over time according to spring dynamics.
- [`useTimeout`](./docs/useTimeout.md) — returns true after a timeout.
- [`useTimeoutFn`](./docs/useTimeoutFn.md) — calls given function after a timeout. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/animation-usetimeoutfn--demo)
- [`useTween`](./docs/useTween.md) — re-renders component, while tweening a number from 0 to 1. [![][img-demo]](https://codesandbox.io/s/52990wwzyl)
- [`useUpdate`](./docs/useUpdate.md) — returns a callback, which re-renders component when called.
<br/>
Expand Down
65 changes: 65 additions & 0 deletions docs/useTimeoutFn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# `useTimeoutFn`

Calls given function after specified amount of milliseconds.
**Note:** this hook does not re-render component by itself.

Automatically cancels timeout on component unmount.
Automatically resets timeout on delay change.

## Usage

```jsx
import * as React from 'react';
import { useTimeoutFn } from 'react-use';

const Demo = () => {
const [state, setState] = React.useState('Not called yet');

function fn() {
setState(`called at ${Date.now()}`);
}

const [isReady, cancel, reset] = useTimeoutFn(fn, 5000);
const cancelButtonClick = useCallback(() => {
if (isReady() === false) {
cancel();
setState(`cancelled`);
} else {
reset();
setState('Not called yet');
}
}, []);

const readyState = isReady();

return (
<div>
<div>{readyState !== null ? 'Function will be called in 5 seconds' : 'Timer cancelled'}</div>
<button onClick={cancelButtonClick}> {readyState === false ? 'cancel' : 'restart'} timeout</button>
<br />
<div>Function state: {readyState === false ? 'Pending' : readyState ? 'Called' : 'Cancelled'}</div>
<div>{state}</div>
</div>
);
};
```

## Reference

```ts
const [
isReady: () => boolean | null,
cancel: () => void,
reset: () => void,
] = useTimeoutFn(fn: Function, ms: number = 0);
```

- **`fn`**_`: Function`_ - function that will be called;
- **`ms`**_`: number`_ - delay in milliseconds;
- **`isReady`**_`: ()=>boolean|null`_ - function returning current timeout state:
- `false` - pending
- `true` - called
- `null` - cancelled
- **`cancel`**_`: ()=>void`_ - cancel the timeout
- **`reset`**_`: ()=>void`_ - reset the timeout

40 changes: 40 additions & 0 deletions src/__stories__/useTimeoutFn.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useCallback } from 'react';
import { useTimeoutFn } from '../index';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const [state, setState] = React.useState('Not called yet');

function fn() {
setState(`called at ${Date.now()}`);
}

const [isReady, cancel, reset] = useTimeoutFn(fn, 5000);
const cancelButtonClick = useCallback(() => {
if (isReady() === false) {
cancel();
setState(`cancelled`);
} else {
reset();
setState('Not called yet');
}
}, []);

const readyState = isReady();

return (
<div>
<div>{readyState !== null ? 'Function will be called in 5 seconds' : 'Timer cancelled'}</div>
<button onClick={cancelButtonClick}> {readyState === false ? 'cancel' : 'restart'} timeout</button>
<br />
<div>Function state: {readyState === false ? 'Pending' : readyState ? 'Called' : 'Cancelled'}</div>
<div>{state}</div>
</div>
);
};

storiesOf('Animation|useTimeoutFn', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useTimeoutFn.md')} />)
.add('Demo', () => <Demo />);
116 changes: 116 additions & 0 deletions src/__tests__/useTimeoutFn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { useTimeoutFn } from '../index';
import { UseTimeoutFnReturn } from '../useTimeoutFn';

describe('useTimeoutFn', () => {
beforeAll(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.clearAllTimers();
});

afterAll(() => {
jest.useRealTimers();
});

it('should be defined', () => {
expect(useTimeoutFn).toBeDefined();
});

it('should return three functions', () => {
const hook = renderHook(() => useTimeoutFn(() => {}, 5));

expect(hook.result.current.length).toBe(3);
expect(typeof hook.result.current[0]).toBe('function');
expect(typeof hook.result.current[1]).toBe('function');
expect(typeof hook.result.current[2]).toBe('function');
});

function getHook(ms: number = 5): [jest.Mock, RenderHookResult<{ delay: number }, UseTimeoutFnReturn>] {
const spy = jest.fn();
return [spy, renderHook(({ delay = 5 }) => useTimeoutFn(spy, delay), { initialProps: { delay: ms } })];
}

it('should call passed function after given amount of time', () => {
const [spy] = getHook();

expect(spy).not.toHaveBeenCalled();
jest.advanceTimersByTime(5);
expect(spy).toHaveBeenCalledTimes(1);
});

it('should cancel function call on unmount', () => {
const [spy, hook] = getHook();

expect(spy).not.toHaveBeenCalled();
hook.unmount();
jest.advanceTimersByTime(5);
expect(spy).not.toHaveBeenCalled();
});

it('first function should return actual state of timeout', () => {
let [, hook] = getHook();
let [isReady] = hook.result.current;

expect(isReady()).toBe(false);
hook.unmount();
expect(isReady()).toBe(null);

[, hook] = getHook();
[isReady] = hook.result.current;
jest.advanceTimersByTime(5);
expect(isReady()).toBe(true);
});

it('second function should cancel timeout', () => {
const [spy, hook] = getHook();
const [isReady, cancel] = hook.result.current;

expect(spy).not.toHaveBeenCalled();
expect(isReady()).toBe(false);

act(() => {
cancel();
});
jest.advanceTimersByTime(5);

expect(spy).not.toHaveBeenCalled();
expect(isReady()).toBe(null);
});

it('third function should reset timeout', () => {
const [spy, hook] = getHook();
const [isReady, cancel, reset] = hook.result.current;

expect(isReady()).toBe(false);

act(() => {
cancel();
});
jest.advanceTimersByTime(5);

expect(isReady()).toBe(null);

act(() => {
reset();
});
expect(isReady()).toBe(false);

jest.advanceTimersByTime(5);

expect(spy).toHaveBeenCalledTimes(1);
expect(isReady()).toBe(true);
});

it('should reset timeout on delay change', () => {
const [spy, hook] = getHook(50);

expect(spy).not.toHaveBeenCalled();
hook.rerender({ delay: 5 });

jest.advanceTimersByTime(5);
expect(spy).toHaveBeenCalledTimes(1);
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export { default as useStartTyping } from './useStartTyping';
export { default as useThrottle } from './useThrottle';
export { default as useThrottleFn } from './useThrottleFn';
export { default as useTimeout } from './useTimeout';
export { default as useTimeoutFn } from './useTimeoutFn';
export { default as useTitle } from './useTitle';
export { default as useToggle } from './useToggle';
export { default as useTween } from './useTween';
Expand Down
29 changes: 29 additions & 0 deletions src/useTimeoutFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useCallback, useEffect, useRef } from 'react';

export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];

export default function useTimeoutFn(fn: Function, ms: number = 0): UseTimeoutFnReturn {
const ready = useRef<boolean | null>(false);
const timeout = useRef(0);

const isReady = useCallback(() => ready.current, []);
const set = useCallback(() => {
ready.current = false;
timeout.current = window.setTimeout(() => {
ready.current = true;
fn();
}, ms);
}, [ms, fn]);
const clear = useCallback(() => {
ready.current = null;
timeout.current && clearTimeout(timeout.current);
}, []);

useEffect(() => {
set();

return clear;
}, [ms]);

return [isReady, clear, set];
}

0 comments on commit 284e6fd

Please sign in to comment.