diff --git a/CHANGELOG.md b/CHANGELOG.md
index 678d0c69..6f4a3c2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+# [11.0.0](https://github.com/streamich/react-use/compare/v10.8.0...v11.0.0) (2019-08-22)
+
+
+### Features
+
+* add cancel and reset methods to useTimeout ([283045a](https://github.com/streamich/react-use/commit/283045a))
+* add useTimeoutFn ([284e6fd](https://github.com/streamich/react-use/commit/284e6fd))
+
+
+### BREAKING CHANGES
+
+* useTimeout now returns a tuple
+
# [10.8.0](https://github.com/streamich/react-use/compare/v10.7.1...v10.8.0) (2019-08-20)
diff --git a/README.md b/README.md
index d24873e0..8e36339e 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,8 @@
- [`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.
diff --git a/docs/useTimeout.md b/docs/useTimeout.md
index 4c4c96e2..6c09e62c 100644
--- a/docs/useTimeout.md
+++ b/docs/useTimeout.md
@@ -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
Ready: {ready ? 'Yes' : 'No'}
;
+ return (
+
+ { isReady() ? 'I\'m reloaded after timeout' : `I will be reloaded after ${ ms / 1000 }s` }
+ { isReady() === false ? Cancel : '' }
+
+ );
+}
+
+const Demo = () => {
+ return (
+
+
+
+
+ );
};
```
+
+## 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
diff --git a/docs/useTimeoutFn.md b/docs/useTimeoutFn.md
new file mode 100644
index 00000000..52d259be
--- /dev/null
+++ b/docs/useTimeoutFn.md
@@ -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 (
+
+
{readyState !== null ? 'Function will be called in 5 seconds' : 'Timer cancelled'}
+
{readyState === false ? 'cancel' : 'restart'} timeout
+
+
Function state: {readyState === false ? 'Pending' : readyState ? 'Called' : 'Cancelled'}
+
{state}
+
+ );
+};
+```
+
+## 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
+
diff --git a/package.json b/package.json
index 6f2f1f5c..4bb3a3de 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-use",
- "version": "10.8.0",
+ "version": "11.0.0",
"description": "Collection of React Hooks",
"main": "lib/index.js",
"module": "esm/index.js",
@@ -96,7 +96,7 @@
"redux-thunk": "2.3.0",
"rimraf": "3.0.0",
"rxjs": "6.5.2",
- "semantic-release": "15.13.21",
+ "semantic-release": "15.13.24",
"ts-loader": "6.0.4",
"ts-node": "8.3.0",
"tslint": "5.19.0",
diff --git a/src/__stories__/useTimeout.story.tsx b/src/__stories__/useTimeout.story.tsx
index a393ee21..99fda16a 100644
--- a/src/__stories__/useTimeout.story.tsx
+++ b/src/__stories__/useTimeout.story.tsx
@@ -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 Ready: {ready ? 'Yes' : 'No'}
;
+ return (
+
+ {isReady() ? "I'm reloaded after timeout" : `I will be reloaded after ${ms / 1000}s`}
+ {isReady() === false ? Cancel : ''}
+
+ );
+}
+
+const Demo = () => {
+ return (
+
+
+
+
+ );
};
storiesOf('Animation|useTimeout', module)
diff --git a/src/__stories__/useTimeoutFn.story.tsx b/src/__stories__/useTimeoutFn.story.tsx
new file mode 100644
index 00000000..9dcb45f3
--- /dev/null
+++ b/src/__stories__/useTimeoutFn.story.tsx
@@ -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 (
+
+
{readyState !== null ? 'Function will be called in 5 seconds' : 'Timer cancelled'}
+
{readyState === false ? 'cancel' : 'restart'} timeout
+
+
Function state: {readyState === false ? 'Pending' : readyState ? 'Called' : 'Cancelled'}
+
{state}
+
+ );
+};
+
+storiesOf('Animation|useTimeoutFn', module)
+ .add('Docs', () => )
+ .add('Demo', () => );
diff --git a/src/__tests__/useTimeout.test.ts b/src/__tests__/useTimeout.test.ts
index dec42fd9..905b4022 100644
--- a/src/__tests__/useTimeout.test.ts
+++ b/src/__tests__/useTimeout.test.ts
@@ -1,83 +1,138 @@
-import { act, renderHook } from '@testing-library/react-hooks';
-import useTimeout from '../useTimeout';
+import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
+import { useTimeout } from '../index';
+import { UseTimeoutReturn } from '../useTimeout';
-beforeEach(() => {
- jest.useFakeTimers();
-});
-
-afterEach(() => {
- jest.clearAllTimers();
-});
-
-it('should init ready bool to false', () => {
- const { result } = renderHook(() => useTimeout());
-
- expect(result.current).toBe(false);
-});
-
-it('should return ready as true on default timeout reached', () => {
- const { result } = renderHook(() => useTimeout());
-
- act(() => {
- // default timeout is 0 so we just basically start running scheduled timer
- jest.advanceTimersByTime(0);
+describe('useTimeout', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
});
- expect(result.current).toBe(true);
-});
-
-it('should return ready as true on custom timeout reached', () => {
- const { result } = renderHook(() => useTimeout(200));
-
- act(() => {
- jest.advanceTimersByTime(200);
- });
-
- expect(result.current).toBe(true);
-});
-
-it('should always return ready as false on custom timeout not reached', () => {
- const { result } = renderHook(() => useTimeout(200));
-
- act(() => {
- jest.advanceTimersByTime(100);
- });
- expect(result.current).toBe(false);
-
- act(() => {
- jest.advanceTimersByTime(90);
- });
- expect(result.current).toBe(false);
-
- act(() => {
- jest.advanceTimersByTime(9);
- });
- expect(result.current).toBe(false);
-});
-
-it('should always return ready as true after custom timeout reached', () => {
- const { result } = renderHook(() => useTimeout(200));
-
- act(() => {
- jest.advanceTimersByTime(200);
- });
- expect(result.current).toBe(true);
-
- act(() => {
- jest.advanceTimersByTime(20);
- });
- expect(result.current).toBe(true);
-
- act(() => {
- jest.advanceTimersByTime(100);
- });
- expect(result.current).toBe(true);
-});
-
-it('should clear pending timer on unmount', () => {
- const { unmount } = renderHook(() => useTimeout());
- expect(jest.getTimerCount()).toBe(1);
-
- unmount();
- expect(jest.getTimerCount()).toBe(0);
+ 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);
+ });
});
diff --git a/src/__tests__/useTimeoutFn.test.ts b/src/__tests__/useTimeoutFn.test.ts
new file mode 100644
index 00000000..8164bcdc
--- /dev/null
+++ b/src/__tests__/useTimeoutFn.test.ts
@@ -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);
+ });
+});
diff --git a/src/index.ts b/src/index.ts
index b4361a8a..644f587e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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';
diff --git a/src/useTimeout.ts b/src/useTimeout.ts
index 84c7a143..387a7c76 100644
--- a/src/useTimeout.ts
+++ b/src/useTimeout.ts
@@ -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);
+}
diff --git a/src/useTimeoutFn.ts b/src/useTimeoutFn.ts
new file mode 100644
index 00000000..16276343
--- /dev/null
+++ b/src/useTimeoutFn.ts
@@ -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(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];
+}
diff --git a/yarn.lock b/yarn.lock
index 354176c8..aa473406 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10948,10 +10948,10 @@ select@^1.1.2:
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
-semantic-release@15.13.21:
- version "15.13.21"
- resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-15.13.21.tgz#d021c75f889cff75ae3410736942bee6c4557da7"
- integrity sha512-3S9thQas28iv3NeHUqQVsDnxMcBGQICdxabeNnJ8BnbRBvCkgqCg3v9zo/+O5a8GCyxrgjtwJ2iWozL8SiIq1w==
+semantic-release@15.13.24:
+ version "15.13.24"
+ resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-15.13.24.tgz#f0b9544427d059ba5e3c89ac1545234130796be7"
+ integrity sha512-OPshm6HSp+KmZP9dUv1o3MRILDgOeHYWPI+XSpQRERMri7QkaEiIPkZzoNm2d6KDeFDnp03GphQQS4+Zfo+x/Q==
dependencies:
"@semantic-release/commit-analyzer" "^6.1.0"
"@semantic-release/error" "^2.2.0"
@@ -10978,7 +10978,7 @@ semantic-release@15.13.21:
resolve-from "^5.0.0"
semver "^6.0.0"
signale "^1.2.1"
- yargs "^13.1.0"
+ yargs "^14.0.0"
semver-compare@^1.0.0:
version "1.0.0"
@@ -12886,7 +12886,7 @@ yargs@^11.0.0:
y18n "^3.2.1"
yargs-parser "^9.0.2"
-yargs@^13.1.0, yargs@^13.3.0:
+yargs@^13.3.0:
version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==
@@ -12902,6 +12902,23 @@ yargs@^13.1.0, yargs@^13.3.0:
y18n "^4.0.0"
yargs-parser "^13.1.1"
+yargs@^14.0.0:
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.0.0.tgz#ba4cacc802b3c0b3e36a9e791723763d57a85066"
+ integrity sha512-ssa5JuRjMeZEUjg7bEL99AwpitxU/zWGAGpdj0di41pOEmJti8NR6kyUIJBkR78DTYNPZOU08luUo0GTHuB+ow==
+ dependencies:
+ cliui "^5.0.0"
+ decamelize "^1.2.0"
+ find-up "^3.0.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^3.0.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^13.1.1"
+
yn@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.0.0.tgz#0073c6b56e92aed652fbdfd62431f2d6b9a7a091"