Skip to content

Commit

Permalink
feat: add cancel and reset methods to useTimeout
Browse files Browse the repository at this point in the history
BREAKING CHANGE: useTimeout now returns a tuple
  • Loading branch information
streamich authored Aug 22, 2019
2 parents 284e6fd + d416499 commit 283045a
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 24 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
- [`useRaf`](./docs/useRaf.md) — re-renders component on each `requestAnimationFrame`.
- [`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.
- [`useTimeout`](./docs/useTimeout.md) — re-renders component 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.
Expand Down
41 changes: 37 additions & 4 deletions docs/useTimeout.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,48 @@
# `useTimeout`

Returns `true` after a specified number of milliseconds.
Re-renders the component after a specified number of milliseconds.
Provides handles to cancel and/or reset the timeout.

## Usage

```jsx
import { useTimeout } from 'react-use';

const Demo = () => {
const ready = useTimeout(2000);
function TestComponent(props: { ms?: number } = {}) {
const ms = props.ms || 5000;
const [isReady, cancel] = useTimeout(ms);

return (
<div>
{ isReady() ? 'I\'m reloaded after timeout' : `I will be reloaded after ${ ms / 1000 }s` }
{ isReady() === false ? <button onClick={ cancel }>Cancel</button> : '' }
</div>
);
}

return <div>Ready: {ready ? 'Yes' : 'No'}</div>;
const Demo = () => {
return (
<div>
<TestComponent />
<TestComponent ms={ 10000 } />
</div>
);
};
```

## Reference

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

- **`isReady`**_` :()=>boolean|null`_ - function returning current timeout state:
- `false` - pending re-render
- `true` - re-render performed
- `null` - re-render cancelled
- **`cancel`**_` :()=>void`_ - cancel the timeout (component will not be re-rendered)
- **`reset`**_` :()=>void`_ - reset the timeout
21 changes: 18 additions & 3 deletions src/__stories__/useTimeout.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,25 @@ import * as React from 'react';
import { useTimeout } from '..';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const ready = useTimeout(2e3);
function TestComponent(props: { ms?: number } = {}) {
const ms = props.ms || 5000;
const [isReady, cancel] = useTimeout(ms);

return (
<div>
{isReady() ? "I'm reloaded after timeout" : `I will be reloaded after ${ms / 1000}s`}
{isReady() === false ? <button onClick={cancel}>Cancel</button> : ''}
</div>
);
}

return <div>Ready: {ready ? 'Yes' : 'No'}</div>;
const Demo = () => {
return (
<div>
<TestComponent />
<TestComponent ms={10000} />
</div>
);
};

storiesOf('Animation|useTimeout', module)
Expand Down
138 changes: 138 additions & 0 deletions src/__tests__/useTimeout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { useTimeout } from '../index';
import { UseTimeoutReturn } from '../useTimeout';

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

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

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

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

it('should return three functions', () => {
const hook = renderHook(() => useTimeout(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 }, UseTimeoutReturn>] {
const spy = jest.fn();
return [
spy,
renderHook(
({ delay = 5 }) => {
spy();
return useTimeout(delay);
},
{ initialProps: { delay: ms } }
),
];
}

it('should re-render component after given amount of time', done => {
const [spy, hook] = getHook();
expect(spy).toHaveBeenCalledTimes(1);
hook.waitForNextUpdate().then(() => {
expect(spy).toHaveBeenCalledTimes(2);
done();
});
jest.advanceTimersByTime(5);
});

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

expect(spy).toHaveBeenCalledTimes(1);
hook.unmount();
jest.advanceTimersByTime(5);
expect(spy).toHaveBeenCalledTimes(1);
});

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

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

[, hook] = getHook();
[isReady] = hook.result.current;
hook.waitForNextUpdate().then(() => {
expect(isReady()).toBe(true);

done();
});
jest.advanceTimersByTime(5);
});

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

expect(spy).toHaveBeenCalledTimes(1);
expect(isReady()).toBe(false);

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

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

it('third function should reset timeout', done => {
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);

hook.waitForNextUpdate().then(() => {
expect(spy).toHaveBeenCalledTimes(2);
expect(isReady()).toBe(true);

done();
});
jest.advanceTimersByTime(5);
});

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

expect(spy).toHaveBeenCalledTimes(1);
hook.rerender({ delay: 5 });

hook.waitForNextUpdate().then(() => {
expect(spy).toHaveBeenCalledTimes(3);

done();
});
jest.advanceTimersByTime(15);
});
});
23 changes: 7 additions & 16 deletions src/useTimeout.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import { useEffect, useState } from 'react';
import useTimeoutFn from './useTimeoutFn';
import useUpdate from './useUpdate';

const useTimeout = (ms: number = 0) => {
const [ready, setReady] = useState(false);
export type UseTimeoutReturn = [() => boolean | null, () => void, () => void];

useEffect(() => {
const timer = setTimeout(() => {
setReady(true);
}, ms);
export default function useTimeout(ms: number = 0): UseTimeoutReturn {
const update = useUpdate();

return () => {
clearTimeout(timer);
};
}, [ms]);

return ready;
};

export default useTimeout;
return useTimeoutFn(update, ms);
}

0 comments on commit 283045a

Please sign in to comment.