diff --git a/docs/useRafLoop.md b/docs/useRafLoop.md
index 978c5587ac..a326600728 100644
--- a/docs/useRafLoop.md
+++ b/docs/useRafLoop.md
@@ -1,26 +1,35 @@
# `useRafLoop`
-React hook that calls given function inside the RAF loop without re-rendering parent component if not needed. Loop stops automatically on component unmount.
-Provides controls to stop and start loop manually.
+This hook call given function within the RAF loop without re-rendering parent component.
+Loop stops automatically on component unmount.
+
+Additionally hook provides methods to start/stop loop and check current state.
## Usage
```jsx
import * as React from 'react';
-import { useRafLoop } from 'react-use';
+import { useRafLoop, useUpdate } from 'react-use';
const Demo = () => {
const [ticks, setTicks] = React.useState(0);
+ const [lastCall, setLastCall] = React.useState(0);
+ const update = useUpdate();
- const [loopStop, isActive, loopStart] = useRafLoop(() => {
- setTicks(ticks + 1);
- }, [ticks]);
+ const [loopStop, loopStart, isActive] = useRafLoop((time) => {
+ setTicks(ticks => ticks + 1);
+ setLastCall(time);
+ });
return (
RAF triggered: {ticks} (times)
+
Last high res timestamp: {lastCall}
-
+
);
};
@@ -29,7 +38,13 @@ const Demo = () => {
## Reference
```ts
-const [stopLoop, isActive, startLoop] = useRafLoop(callback: CallableFunction, deps?: DependencyList);
+const [stopLoop, startLoop, isActive] = useRafLoop(callback: FrameRequestCallback, initiallyActive = true);
```
-* `callback(time: number)` — function to call each RAF tick
+* **`callback`**_: `(time: number)=>void`_ — function to call each RAF tick.
+ * **`time`**_: `number`_ — DOMHighResTimeStamp, which indicates the current time (based on the number of milliseconds since time origin).
+* **`initiallyActive`**_: `boolean`_ — whether loop should be started at initial render.
+* Return
+ * **`stopLoop`**_: `()=>void`_ — stop loop if it is active.
+ * **`startLoop`**_: `()=>void`_ — start loop if it was inactive.
+ * **`isActive`**_: `()=>boolean`_ — _true_ if loop is active.
diff --git a/package.json b/package.json
index 7d54acbd1e..e1187107aa 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"test": "jest --maxWorkers 2",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
- "lint": "eslint '{src,tests}/**/*.{ts,tsx}'",
+ "lint": "eslint {src,tests}/**/*.{ts,tsx}",
"lint:fix": "yarn lint --fix",
"lint:types": "tsc --noEmit",
"build:cjs": "tsc",
diff --git a/src/useRafLoop.ts b/src/useRafLoop.ts
index 9760ea6b7b..444d4ee14e 100644
--- a/src/useRafLoop.ts
+++ b/src/useRafLoop.ts
@@ -1,36 +1,45 @@
-import { useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef } from 'react';
-export type RafLoopReturns = [() => void, boolean, () => void];
+export type RafLoopReturns = [() => void, () => void, () => boolean];
-export default function useRafLoop(callback: CallableFunction): RafLoopReturns {
+export default function useRafLoop(callback: FrameRequestCallback, initiallyActive = true): RafLoopReturns {
const raf = useRef(null);
- const [isActive, setIsActive] = useState(true);
-
- function loopStep(time: number) {
- callback(time);
- raf.current = requestAnimationFrame(loopStep);
- }
-
- function loopStop() {
- setIsActive(false);
- }
-
- function loopStart() {
- setIsActive(true);
- }
-
- function clearCurrentLoop() {
- raf.current && cancelAnimationFrame(raf.current);
- }
-
- useEffect(() => clearCurrentLoop, []);
+ const rafActivity = useRef(false);
+ const rafCallback = useRef(callback);
+ rafCallback.current = callback;
+
+ const step = useCallback((time: number) => {
+ if (rafActivity.current) {
+ rafCallback.current(time);
+ raf.current = requestAnimationFrame(step);
+ }
+ }, []);
+
+ const result = useMemo(() => ([
+ () => { // stop
+ if (rafActivity.current) {
+ rafActivity.current = false;
+ raf.current && cancelAnimationFrame(raf.current);
+ }
+ },
+ () => { // start
+ if (!rafActivity.current) {
+ rafActivity.current = true;
+ raf.current = requestAnimationFrame(step);
+ }
+ },
+ (): boolean => rafActivity.current // isActive
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ ] as RafLoopReturns), []);
useEffect(() => {
- clearCurrentLoop();
- isActive && (raf.current = requestAnimationFrame(loopStep));
+ if (initiallyActive) {
+ result[1]();
+ }
- return clearCurrentLoop;
- }, [isActive, callback]);
+ return result[0];
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
- return [loopStop, isActive, loopStart];
+ return result;
}
diff --git a/stories/useRafLoop.story.tsx b/stories/useRafLoop.story.tsx
index 076e35a5f1..0f860c8ae7 100644
--- a/stories/useRafLoop.story.tsx
+++ b/stories/useRafLoop.story.tsx
@@ -1,20 +1,27 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
-import { useRafLoop } from '../src';
+import { useRafLoop, useUpdate } from '../src';
import ShowDocs from './util/ShowDocs';
const Demo = () => {
const [ticks, setTicks] = React.useState(0);
+ const [lastCall, setLastCall] = React.useState(0);
+ const update = useUpdate();
- const [loopStop, isActive, loopStart] = useRafLoop(() => {
- setTicks(ticks + 1);
+ const [loopStop, loopStart, isActive] = useRafLoop((time) => {
+ setTicks(ticks => ticks + 1);
+ setLastCall(time);
});
return (
RAF triggered: {ticks} (times)
+
Last high res timestamp: {lastCall}
-
+
);
};
diff --git a/tests/useRafLoop.test.tsx b/tests/useRafLoop.test.tsx
index 36f7d4466f..e61e26835d 100644
--- a/tests/useRafLoop.test.tsx
+++ b/tests/useRafLoop.test.tsx
@@ -1,4 +1,4 @@
-import { act, renderHook } from '@testing-library/react-hooks';
+import { renderHook } from '@testing-library/react-hooks';
import { replaceRaf } from 'raf-stub';
import useRafLoop from '../src/useRafLoop';
@@ -23,96 +23,129 @@ describe('useRafLoop', () => {
expect(useRafLoop).toBeDefined();
});
- it('should return stop function, start function and loop state', () => {
+ it('should return object with start, stop and isActive functions', () => {
const hook = renderHook(() => useRafLoop(() => false), { initialProps: false });
- expect(typeof hook.result.current[0]).toEqual('function');
- expect(typeof hook.result.current[1]).toEqual('boolean');
- expect(typeof hook.result.current[2]).toEqual('function');
+ expect(hook.result.current).toStrictEqual([
+ expect.any(Function),
+ expect.any(Function),
+ expect.any(Function),
+ ]);
});
- it('should call a callback constantly inside the raf loop', () => {
+ it('should constantly call callback inside the raf loop', () => {
const spy = jest.fn();
renderHook(() => useRafLoop(spy), { initialProps: false });
expect(spy).not.toBeCalled();
- requestAnimationFrame.step();
- requestAnimationFrame.step();
+ requestAnimationFrame.step(2);
expect(spy).toBeCalledTimes(2);
+ requestAnimationFrame.step(2);
+ expect(spy).toBeCalledTimes(4);
});
- it('first element call should stop the loop', () => {
+ it('should not start the loop if 2nd hook parameter is falsy', () => {
const spy = jest.fn();
- const hook = renderHook(() => useRafLoop(spy), { initialProps: false });
+ renderHook(() => useRafLoop(spy, false), { initialProps: false });
expect(spy).not.toBeCalled();
-
- act(() => {
- hook.result.current[0]();
- });
- requestAnimationFrame.step();
+ requestAnimationFrame.step(2);
expect(spy).not.toBeCalled();
});
- it('second element should represent loop state', () => {
+ it('should pass the time argument to given callback', () => {
const spy = jest.fn();
- const hook = renderHook(() => useRafLoop(spy), { initialProps: false });
-
- expect(hook.result.current[1]).toBe(true);
+ renderHook(() => useRafLoop(spy), { initialProps: false });
- // stop the loop
- act(() => {
- hook.result.current[0]();
- });
- expect(hook.result.current[1]).toBe(false);
+ expect(spy).not.toBeCalled();
+ requestAnimationFrame.step();
+ expect(typeof spy.mock.calls[0][0]).toBe('number');
});
- it('third element call should restart loop', () => {
+ it('should stop the loop on component unmount', () => {
const spy = jest.fn();
const hook = renderHook(() => useRafLoop(spy), { initialProps: false });
expect(spy).not.toBeCalled();
- // stop the loop
- act(() => {
- hook.result.current[0]();
- });
- requestAnimationFrame.step();
- expect(spy).not.toBeCalled();
+ requestAnimationFrame.step(2);
+ expect(spy).toBeCalledTimes(2);
- // start the loop
- act(() => {
- hook.result.current[2]();
- });
+ hook.unmount();
- requestAnimationFrame.step();
- requestAnimationFrame.step();
+ requestAnimationFrame.step(2);
expect(spy).toBeCalledTimes(2);
});
- it('loop should stop itself on unmount', () => {
- const spy = jest.fn();
- const hook = renderHook(() => useRafLoop(spy), { initialProps: false });
+ it('should call the actual callback when it changed', () => {
+ const spy1 = jest.fn();
+ const spy2 = jest.fn();
+ const hook = renderHook(({cb}) => useRafLoop(cb), { initialProps: {cb: spy1} });
- hook.unmount();
+ expect(spy1).not.toBeCalled();
+ requestAnimationFrame.step(2);
+ expect(spy1).toBeCalledTimes(2);
- requestAnimationFrame.step();
+ hook.rerender({cb: spy2});
- expect(spy).not.toBeCalled();
+ requestAnimationFrame.step(2);
+ expect(spy1).toBeCalledTimes(2);
+ expect(spy2).toBeCalledTimes(2);
});
- it('should pass timestamp as 1st argument of callback', () => {
- const spy = jest.fn();
- const hook = renderHook(() => useRafLoop(spy), { initialProps: false });
+ describe('returned methods', () => {
+ it('stop method should stop the loop', () => {
+ const spy = jest.fn();
+ const hook = renderHook(() => useRafLoop(spy), { initialProps: false });
- requestAnimationFrame.step();
+ const [stop] = hook.result.current;
- act(() => {
- hook.result.current[0]();
+ expect(spy).not.toBeCalled();
+ requestAnimationFrame.step(2);
+ expect(spy).toBeCalledTimes(2);
+
+ stop();
+
+ requestAnimationFrame.step(2);
+ expect(spy).toBeCalledTimes(2);
});
- requestAnimationFrame.step();
+ it('start method should start stopped loop', () => {
+ const spy = jest.fn();
+ const hook = renderHook(() => useRafLoop(spy, false), { initialProps: false });
- expect(spy).toHaveBeenCalled();
- expect(typeof spy.mock.calls[0][0]).toBe('number');
+ const [stop, start] = hook.result.current;
+
+ expect(spy).not.toBeCalled();
+ requestAnimationFrame.step(2);
+ expect(spy).not.toBeCalled();
+
+ start();
+
+ requestAnimationFrame.step(2);
+ expect(spy).toBeCalledTimes(2);
+
+ stop();
+
+ requestAnimationFrame.step(2);
+ expect(spy).toBeCalledTimes(2);
+
+ start();
+
+ requestAnimationFrame.step(2);
+ expect(spy).toBeCalledTimes(4);
+ });
+
+ it('isActive method should return current loop state', () => {
+ const spy = jest.fn();
+ const hook = renderHook(() => useRafLoop(spy, false), { initialProps: false });
+
+ const [stop, start, isActive] = hook.result.current;
+
+ expect(isActive()).toBe(false);
+ start();
+ expect(isActive()).toBe(true);
+ stop();
+ expect(isActive()).toBe(false);
+ });
});
});