diff --git a/README.md b/README.md
index d24873e0..a812c062 100644
--- a/README.md
+++ b/README.md
@@ -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.
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'}
+
+
+
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/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'}
+
+
+
Function state: {readyState === false ? 'Pending' : readyState ? 'Called' : 'Cancelled'}
+
{state}
+
+ );
+};
+
+storiesOf('Animation|useTimeoutFn', module)
+ .add('Docs', () => )
+ .add('Demo', () => );
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/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];
+}