diff --git a/src/hooks/useAnimationLoop/useAnimationLoop.stories.mdx b/src/hooks/useAnimationLoop/useAnimationLoop.stories.mdx
new file mode 100644
index 0000000..5eaa337
--- /dev/null
+++ b/src/hooks/useAnimationLoop/useAnimationLoop.stories.mdx
@@ -0,0 +1,61 @@
+import { Meta } from '@storybook/blocks';
+
+
+
+# useAnimationLoop
+
+`useAnimationLoop` hook is a wrapper around requestAnimationFrame function. This hook will execute
+the passed callback function every frame.
+
+## Reference
+
+```ts
+function useAnimationLoop(callback: (delta: number) => void, enabled = false): void;
+```
+
+### Parameters
+
+- `callback` - function accepts `delta` parameter which represents time passed since last
+ invocation;
+- `enabled` - boolean which is used to play and pause the requestAnimationFrame
+
+### Returns
+
+- `void`
+
+## Usage
+
+```tsx
+function DemoComponent(): ReactElement {
+ const [currentTimestamp, setCurrentTimestamp] = useState(Date.now);
+ const [isRunning, toggleIsRunning] = useToggle(true);
+ useAnimationLoop(() => {
+ setCurrentTimestamp(Date.now);
+ }, isRunning);
+
+ return (
+
+
+
Instructions!
+
Click on the button to toggle the animation loop
+
+
+
Test Area
+
+
{currentTimestamp}
+
+
+
+
+
+ );
+}
+```
diff --git a/src/hooks/useAnimationLoop/useAnimationLoop.stories.tsx b/src/hooks/useAnimationLoop/useAnimationLoop.stories.tsx
new file mode 100644
index 0000000..2c6fdd8
--- /dev/null
+++ b/src/hooks/useAnimationLoop/useAnimationLoop.stories.tsx
@@ -0,0 +1,52 @@
+/* eslint-disable react/jsx-no-literals */
+import type { StoryObj } from '@storybook/react';
+import { useState, type ReactElement } from 'react';
+import { useToggle } from '../useToggle/useToggle.js';
+import { useAnimationLoop } from './useAnimationLoop.js';
+
+export default {
+ title: 'hooks/useAnimationLoop',
+};
+
+function DemoComponent(): ReactElement {
+ const [timestamp, setTimestamp] = useState(0);
+ const [delta, setDelta] = useState(0);
+ const [isRunning, toggleIsRunning] = useToggle(true);
+
+ useAnimationLoop((_timestamp: DOMHighResTimeStamp) => {
+ setDelta(_timestamp - timestamp);
+ setTimestamp(_timestamp);
+ }, isRunning);
+
+ return (
+
+
+
Instructions!
+
Click on the button to toggle the animation loop
+
+
+
Test Area
+
+
Timestamp: {timestamp}
+
Delta: {delta}
+
+
+
+
+
+ );
+}
+
+export const Demo: StoryObj = {
+ render() {
+ return ;
+ },
+};
diff --git a/src/hooks/useAnimationLoop/useAnimationLoop.test.tsx b/src/hooks/useAnimationLoop/useAnimationLoop.test.tsx
new file mode 100644
index 0000000..a2c1304
--- /dev/null
+++ b/src/hooks/useAnimationLoop/useAnimationLoop.test.tsx
@@ -0,0 +1,106 @@
+import { jest } from '@jest/globals';
+import { renderHook, waitFor } from '@testing-library/react';
+import { useAnimationLoop } from './useAnimationLoop.js';
+
+describe('useAnimationLoop', () => {
+ it('should not crash', async () => {
+ renderHook(useAnimationLoop, {
+ initialProps: undefined,
+ });
+ });
+
+ it('should not execute the callback function when enabled is set to false', async () => {
+ const spy = jest.fn();
+
+ renderHook(
+ ({ callback }) => {
+ useAnimationLoop(callback, false);
+ },
+ {
+ initialProps: {
+ callback: spy,
+ },
+ },
+ );
+
+ await waitFor(() => {
+ expect(spy).toBeCalledTimes(0);
+ });
+ });
+
+ it('should execute the callback function when useAnimationLoop is enabled', async () => {
+ const spy = jest.fn();
+
+ renderHook(
+ ({ callback }) => {
+ useAnimationLoop(callback);
+ },
+ {
+ initialProps: {
+ callback: spy,
+ },
+ },
+ );
+
+ await waitFor(() => {
+ expect(spy).toBeCalled();
+ });
+ });
+
+ it('should not execute previous callback function when callback function is updated', async () => {
+ const spyFirstRender = jest.fn();
+ const spySecondRender = jest.fn();
+
+ const { rerender } = renderHook(
+ ({ callback }) => {
+ useAnimationLoop(callback);
+ },
+ {
+ initialProps: {
+ callback: spyFirstRender,
+ },
+ },
+ );
+
+ await waitFor(() => {
+ expect(spyFirstRender).toBeCalled();
+ expect(spySecondRender).toBeCalledTimes(0);
+ });
+
+ rerender({ callback: spySecondRender });
+ const amountOfCalls = spyFirstRender.mock.calls.length;
+
+ await waitFor(() => {
+ expect(spyFirstRender).toBeCalledTimes(amountOfCalls);
+ expect(spySecondRender).toBeCalled();
+ });
+ });
+
+ it('should execute the callback function when useAnimationLoop is enabled and should not execute the callback function when useAnimationLoop is disabled', async () => {
+ const spy = jest.fn();
+
+ const { rerender } = renderHook(
+ ({ callback, enabled }) => {
+ useAnimationLoop(callback, enabled);
+ },
+ {
+ initialProps: {
+ callback: spy,
+ enabled: true,
+ },
+ },
+ );
+
+ await waitFor(() => {
+ expect(spy).toBeCalled();
+ });
+
+ rerender({ callback: spy, enabled: false });
+
+ const amountOfCalls = spy.mock.calls.length;
+
+ await waitFor(() => {
+ expect(spy).toBeCalledTimes(amountOfCalls);
+ });
+ });
+});
diff --git a/src/hooks/useAnimationLoop/useAnimationLoop.ts b/src/hooks/useAnimationLoop/useAnimationLoop.ts
new file mode 100644
index 0000000..6f78f5d
--- /dev/null
+++ b/src/hooks/useAnimationLoop/useAnimationLoop.ts
@@ -0,0 +1,34 @@
+import { useCallback, useEffect, useRef } from 'react';
+import { useRefValue } from '../useRefValue/useRefValue.js';
+
+/**
+ * useAnimationLoop hook is a wrapper around requestAnimationFrame method.
+ * This hook will execute a callback function every next frame.
+ *
+ * @param callback - callback function with @param delta which represents time passed since last invocation
+ * @param enabled - boolean which is used to play and pause the requestAnimationFrame
+ */
+export function useAnimationLoop(callback: FrameRequestCallback, enabled = true): void {
+ const animationFrameRef = useRef(0);
+ const callbackRef = useRefValue(callback);
+
+ const tick = useCallback(
+ (time) => {
+ callbackRef.current?.(time);
+ animationFrameRef.current = requestAnimationFrame(tick);
+ },
+ [callbackRef],
+ );
+
+ useEffect(() => {
+ if (enabled) {
+ requestAnimationFrame(tick);
+ } else {
+ cancelAnimationFrame(animationFrameRef.current);
+ }
+
+ return () => {
+ cancelAnimationFrame(animationFrameRef.current);
+ };
+ }, [enabled, tick]);
+}