Repository: streamich/react-use
Pull request #568 (2019-08-26 06:59:31)

fix: make useTimeoutFn work properly:
- reset function should clear previous timeout;
- function change should not reset the timeout;


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05b435e4..bca464d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,17 +1,3 @@
-## [11.3.2](https://github.com/streamich/react-use/compare/v11.3.1...v11.3.2) (2019-08-25)
-
-
-### Bug Fixes
-
-* useThrottle & useThrottleFn proper timeout type; ([e2cdb94](https://github.com/streamich/react-use/commit/e2cdb94))
-
-## [11.3.1](https://github.com/streamich/react-use/compare/v11.3.0...v11.3.1) (2019-08-25)
-
-
-### Bug Fixes
-
-* update createReducer to fix initial state ([fd083f2](https://github.com/streamich/react-use/commit/fd083f2))
-
 # [11.3.0](https://github.com/streamich/react-use/compare/v11.2.0...v11.3.0) (2019-08-25)
 
 
diff --git a/docs/createReducer.md b/docs/createReducer.md
index 2b53c645..0d0c17db 100644
--- a/docs/createReducer.md
+++ b/docs/createReducer.md
@@ -45,7 +45,7 @@ const Demo = ({ initialCount = 1 }) => {
       <p>count: {state.count}</p>
       <button onClick={() => dispatch(addAndReset())}>Add and reset</button>
       <button
-        onClick={() => dispatch({ type: 'reset', payload: { count: initialCount }})}
+        onClick={() => dispatch({ type: 'reset', payload: initialCount })}
       >
         Reset
       </button>
diff --git a/docs/useTimeoutFn.md b/docs/useTimeoutFn.md
index 52d259be..48e4bc2d 100644
--- a/docs/useTimeoutFn.md
+++ b/docs/useTimeoutFn.md
@@ -1,10 +1,13 @@
 # `useTimeoutFn`
 
-Calls given function after specified amount of milliseconds.  
-**Note:** this hook does not re-render component by itself.
+Calls given function after specified amount of milliseconds.
 
-Automatically cancels timeout on component unmount.
-Automatically resets timeout on delay change.
+Several thing about it's work:
+- does not re-render component;
+- automatically cancel timeout on cancel;
+- automatically reset timeout on delay change;
+- reset function call will cancel previous timeout;
+- timeout will NOT be reset on function change. It will be called within the timeout, you have to reset it on your own when needed. 
 
 ## Usage
 
diff --git a/package.json b/package.json
index 8c2d7556..8bafdb9f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "react-use",
-  "version": "11.3.2",
+  "version": "11.3.0",
   "description": "Collection of React Hooks",
   "main": "lib/index.js",
   "module": "esm/index.js",
diff --git a/src/useThrottle.ts b/src/useThrottle.ts
index b231ba62..38aaed04 100644
--- a/src/useThrottle.ts
+++ b/src/useThrottle.ts
@@ -3,7 +3,7 @@ import useUnmount from './useUnmount';
 
 const useThrottle = <T>(value: T, ms: number = 200) => {
   const [state, setState] = useState<T>(value);
-  const timeout = useRef<ReturnType<typeof setTimeout>>();
+  const timeout = useRef<any>(null);
   const nextValue = useRef(null) as any;
   const hasNextValue = useRef(0) as any;
 
@@ -16,7 +16,7 @@ const useThrottle = <T>(value: T, ms: number = 200) => {
           setState(nextValue.current);
           timeout.current = setTimeout(timeoutCallback, ms);
         } else {
-          timeout.current = undefined;
+          timeout.current = null;
         }
       };
       timeout.current = setTimeout(timeoutCallback, ms);
@@ -27,7 +27,7 @@ const useThrottle = <T>(value: T, ms: number = 200) => {
   }, [value]);
 
   useUnmount(() => {
-    timeout.current && clearTimeout(timeout.current);
+    clearTimeout(timeout.current);
   });
 
   return state;
diff --git a/src/useThrottleFn.ts b/src/useThrottleFn.ts
index fafbe7b8..9c749855 100644
--- a/src/useThrottleFn.ts
+++ b/src/useThrottleFn.ts
@@ -3,7 +3,7 @@ import useUnmount from './useUnmount';
 
 const useThrottleFn = <T>(fn: (...args: any[]) => T, ms: number = 200, args: any[]) => {
   const [state, setState] = useState<T>(null as any);
-  const timeout = useRef<ReturnType<typeof setTimeout>>();
+  const timeout = useRef<any>(null);
   const nextArgs = useRef(null) as any;
   const hasNextArgs = useRef(false) as any;
 
@@ -16,7 +16,7 @@ const useThrottleFn = <T>(fn: (...args: any[]) => T, ms: number = 200, args: any
           setState(fn(...nextArgs.current));
           timeout.current = setTimeout(timeoutCallback, ms);
         } else {
-          timeout.current = undefined;
+          timeout.current = null;
         }
       };
       timeout.current = setTimeout(timeoutCallback, ms);
@@ -27,7 +27,7 @@ const useThrottleFn = <T>(fn: (...args: any[]) => T, ms: number = 200, args: any
   }, args);
 
   useUnmount(() => {
-    timeout.current && clearTimeout(timeout.current);
+    clearTimeout(timeout.current);
   });
 
   return state;
diff --git a/src/useTimeoutFn.ts b/src/useTimeoutFn.ts
index 8bede34f..4f9e8c50 100644
--- a/src/useTimeoutFn.ts
+++ b/src/useTimeoutFn.ts
@@ -5,20 +5,31 @@ 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<ReturnType<typeof setTimeout>>();
+  const callback = useRef(fn);
 
   const isReady = useCallback(() => ready.current, []);
+
   const set = useCallback(() => {
     ready.current = false;
+    timeout.current && clearTimeout(timeout.current);
+
     timeout.current = setTimeout(() => {
       ready.current = true;
-      fn();
+      callback.current();
     }, ms);
-  }, [ms, fn]);
+  }, [ms]);
+
   const clear = useCallback(() => {
     ready.current = null;
     timeout.current && clearTimeout(timeout.current);
   }, []);
 
+  // update ref when function changes
+  useEffect(() => {
+    callback.current = fn;
+  }, [fn]);
+
+  // set on mount, clear on unmount
   useEffect(() => {
     set();
 


Test patch:
diff --git a/src/__tests__/useTimeoutFn.test.ts b/src/__tests__/useTimeoutFn.test.ts
index 8164bcdc..3c0b616f 100644
--- a/src/__tests__/useTimeoutFn.test.ts
+++ b/src/__tests__/useTimeoutFn.test.ts
@@ -28,9 +28,11 @@ describe('useTimeoutFn', () => {
     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 } })];
+  function getHook(
+    ms: number = 5,
+    fn: Function = jest.fn()
+  ): [Function, RenderHookResult<{ delay: number; cb: Function }, UseTimeoutFnReturn>] {
+    return [fn, renderHook(({ delay = 5, cb }) => useTimeoutFn(cb, delay), { initialProps: { delay: ms, cb: fn } })];
   }
 
   it('should call passed function after given amount of time', () => {
@@ -108,9 +110,23 @@ describe('useTimeoutFn', () => {
     const [spy, hook] = getHook(50);
 
     expect(spy).not.toHaveBeenCalled();
-    hook.rerender({ delay: 5 });
+    hook.rerender({ delay: 5, cb: spy });
 
     jest.advanceTimersByTime(5);
     expect(spy).toHaveBeenCalledTimes(1);
   });
+
+  it('should NOT reset timeout on function change', () => {
+    const [spy, hook] = getHook(50);
+
+    jest.advanceTimersByTime(25);
+    expect(spy).not.toHaveBeenCalled();
+
+    const spy2 = jest.fn();
+    hook.rerender({ delay: 50, cb: spy2 });
+
+    jest.advanceTimersByTime(25);
+    expect(spy).not.toHaveBeenCalled();
+    expect(spy2).toHaveBeenCalledTimes(1);
+  });
 });