diff --git a/.templates/create/hook/package.json.ejs b/.templates/create/hook/package.json.ejs
index 199ca881..c1925b9d 100644
--- a/.templates/create/hook/package.json.ejs
+++ b/.templates/create/hook/package.json.ejs
@@ -21,7 +21,7 @@ to: hooks/<%= h.changeCase.paramCase(name) %>/package.json
"url": "git+https://github.com/Byndyusoft/ui.git"
},
"scripts": {
- "build": "tsc",
+ "build": "tsc --project tsconfig.build.json",
"clean": "rimraf dist",
"lint": "eslint src --config ../../eslint.config.js",
"test": "jest --config ../../jest.config.js --roots components/<%= h.changeCase.paramCase(name) %>/src"
diff --git a/.templates/create/hook/tsconfig.build.json.ejs b/.templates/create/hook/tsconfig.build.json.ejs
new file mode 100644
index 00000000..de616aa3
--- /dev/null
+++ b/.templates/create/hook/tsconfig.build.json.ejs
@@ -0,0 +1,7 @@
+---
+to: hooks/<%= h.changeCase.paramCase(name) %>/tsconfig.build.json
+---
+{
+"extends": "./tsconfig.json",
+"exclude": ["src/*.tests.ts", "src/*.stories.tsx"]
+}
diff --git a/.templates/create/hook/tsconfig.json.ejs b/.templates/create/hook/tsconfig.json.ejs
index b86ee394..bf7d6f68 100644
--- a/.templates/create/hook/tsconfig.json.ejs
+++ b/.templates/create/hook/tsconfig.json.ejs
@@ -10,12 +10,5 @@ to: hooks/<%= h.changeCase.paramCase(name) %>/tsconfig.json
"module": "commonjs",
"target": "es6"
},
- "include": [
- "src"
- ],
- "exclude": [
- "node_modules",
- "src/**/*.tests.tsx",
- "src/**/*.stories.tsx"
- ]
+ "include": ["src", "src/*.stories.tsx"]
}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 094436c4..8f80c315 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -56,7 +56,7 @@ component-name // Название компонента в kebab case
| ├── ComponentName.types.ts
| ├── ComponentName.tests.tsx
| ├── ComponentName.stories.tsx
-| ├── ComponentName.stories.css // стили историй
+| ├── ComponentName.stories.module.css // стили историй
| ├── ComponentName.docs.mdx // документация компонента
| └── index.ts
├── README.md
@@ -78,7 +78,7 @@ use-hook-name // Название хука в kebab case
| ├── useHookName.utilities.ts // логика и методы хука
| ├── useHookName.tests.ts
| ├── useHookName.stories.tsx
-| ├── useHookName.stories.css
+| ├── useHookName.stories.module.css // стили историй
| └── useHookName.docs.mdx // документация хука
├── README.md
├── package.json
@@ -176,7 +176,7 @@ src
"module": "commonjs",
"target": "es6"
},
- "include": ["src"]
+ "include": ["src", "src/*.stories.tsx"]
}
```
diff --git a/hooks/use-array/src/useArray.stories.css b/hooks/use-array/src/useArray.stories.module.css
similarity index 95%
rename from hooks/use-array/src/useArray.stories.css
rename to hooks/use-array/src/useArray.stories.module.css
index e38cb476..e8aa9378 100644
--- a/hooks/use-array/src/useArray.stories.css
+++ b/hooks/use-array/src/useArray.stories.module.css
@@ -15,7 +15,7 @@
flex-grow: 1;
}
-button {
+.button {
flex-basis: 100px;
flex-shrink: 0;
}
diff --git a/hooks/use-array/src/useArray.stories.tsx b/hooks/use-array/src/useArray.stories.tsx
index 8b736f63..b2b9b95a 100644
--- a/hooks/use-array/src/useArray.stories.tsx
+++ b/hooks/use-array/src/useArray.stories.tsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { StoryObj } from '@storybook/react';
import useArray from './useArray';
-import './useArray.stories.css';
+import styles from './useArray.stories.module.css';
const Template = (): JSX.Element => {
const [addValue, setAddValue] = useState(0);
@@ -18,81 +18,121 @@ const Template = (): JSX.Element => {
List
{list.length > 0 ? list.join(', ') : 'Empty'}
-
-
+
+
setAddValue(Number(e.target.value))}
/>
-
-
+
setPrependValue(Number(e.target.value))}
/>
- prepend(prependValue)}>Prepend
+ prepend(prependValue)}
+ >
+ Prepend
+
-
+
from
setFromValue(Number(e.target.value))}
/>
to
setLessValue(Number(e.target.value))}
/>
- filter(a => a >= fromValue && a <= lessValue)}>Filter
+ filter(a => a >= fromValue && a <= lessValue)}
+ >
+ Filter
+
-
+
index
setIndexUpdateValue(Number(e.target.value))}
/>
item
setUpdateValue(Number(e.target.value))}
/>
- update(indexUpdateValue, updateValue)}>Update
+ update(indexUpdateValue, updateValue)}
+ >
+ Update
+
-
+
setIndexRemoveValue(Number(e.target.value))}
/>
- remove(indexRemoveValue)}>Remove
+ remove(indexRemoveValue)}
+ >
+ Remove
+
-
-
reset()}>
+
+ reset()}
+ >
Reset
- clear()}>
+ clear()}
+ >
Clear
- sort((a, b) => a - b)}>
+ sort((a, b) => a - b)}
+ >
Sort ASC
- sort((a, b) => b - a)}>
+ sort((a, b) => b - a)}
+ >
Sort DES
diff --git a/hooks/use-array/tsconfig.json b/hooks/use-array/tsconfig.json
index 5b7870da..ad2f569e 100644
--- a/hooks/use-array/tsconfig.json
+++ b/hooks/use-array/tsconfig.json
@@ -7,5 +7,5 @@
"module": "commonjs",
"target": "es6"
},
- "include": ["src"]
+ "include": ["src", "../../types.d.ts"]
}
diff --git a/hooks/use-debounced-callback/.npmignore b/hooks/use-debounced-callback/.npmignore
new file mode 100644
index 00000000..85de9cf9
--- /dev/null
+++ b/hooks/use-debounced-callback/.npmignore
@@ -0,0 +1 @@
+src
diff --git a/hooks/use-debounced-callback/README.md b/hooks/use-debounced-callback/README.md
new file mode 100644
index 00000000..49644f8b
--- /dev/null
+++ b/hooks/use-debounced-callback/README.md
@@ -0,0 +1,34 @@
+# `@byndyusoft-ui/use-debounced-callback`
+---
+
+> A React hook that uses for delaying the execution of functions updates until a specified time period has passed without any further changes
+
+### Installation
+
+```
+npm i @byndyusoft-ui/use-debounced-callback
+# or
+yarn add @byndyusoft-ui/use-debounced-callback
+```
+
+### Usage
+
+```ts
+type THookReturn = [T, (arg: T) => void];
+
+const useDebouncedValue = (value: T, delay = 300): THookReturn => {
+ const [debouncedValue, setValue] = useState(value);
+
+ const setDebouncedValue = useDebouncedCallback(setValue, delay);
+
+ return [debouncedValue, setDebouncedValue];
+};
+```
+
+### License
+
+Apache-2.0
+
+### Authors
+
+Anastasia Vasenina, Viktor Smorodin
\ No newline at end of file
diff --git a/hooks/use-debounced-callback/package.json b/hooks/use-debounced-callback/package.json
new file mode 100644
index 00000000..f9f1d67b
--- /dev/null
+++ b/hooks/use-debounced-callback/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@byndyusoft-ui/use-debounced-callback",
+ "version": "0.0.0",
+ "description": "Byndyusoft UI React Hook",
+ "keywords": [
+ "byndyusoft",
+ "byndyusoft-ui",
+ "react",
+ "hook",
+ "debounce"
+ ],
+ "author": "Anastasia Vasenina ",
+ "homepage": "https://github.com/Byndyusoft/ui/tree/master/hooks/use-debounced-callback#readme",
+ "license": "Apache-2.0",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Byndyusoft/ui.git"
+ },
+ "scripts": {
+ "build": "tsc --project tsconfig.build.json",
+ "clean": "rimraf dist",
+ "lint": "eslint src --config ../../eslint.config.js",
+ "test": "jest --config ../../jest.config.js --roots hooks/use-debounced-callback/src"
+ },
+ "bugs": {
+ "url": "https://github.com/Byndyusoft/ui/issues"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@byndyusoft-ui/use-timeout": "*"
+ },
+ "peerDependencies": {
+ "@byndyusoft-ui/types": "*"
+ }
+}
diff --git a/hooks/use-debounced-callback/src/index.ts b/hooks/use-debounced-callback/src/index.ts
new file mode 100644
index 00000000..06c38de5
--- /dev/null
+++ b/hooks/use-debounced-callback/src/index.ts
@@ -0,0 +1 @@
+export { default } from './useDebouncedCallback';
diff --git a/hooks/use-debounced-callback/src/useDebouncedCallback.docs.mdx b/hooks/use-debounced-callback/src/useDebouncedCallback.docs.mdx
new file mode 100644
index 00000000..85e72e81
--- /dev/null
+++ b/hooks/use-debounced-callback/src/useDebouncedCallback.docs.mdx
@@ -0,0 +1,19 @@
+import { Markdown, Source, Canvas, Meta } from '@storybook/blocks';
+import * as useDebouncedCallbackStories from './useDebouncedCallback.stories';
+import Readme from '../README.md';
+
+
+
+{Readme}
+
+## Guide
+
+To use the hook in your project you must:
+
+1. Import the hook where you need it:
+
+
+
+2. Give callback and delay args.
+
+
diff --git a/hooks/use-debounced-callback/src/useDebouncedCallback.stories.module.css b/hooks/use-debounced-callback/src/useDebouncedCallback.stories.module.css
new file mode 100644
index 00000000..0a084fd5
--- /dev/null
+++ b/hooks/use-debounced-callback/src/useDebouncedCallback.stories.module.css
@@ -0,0 +1,21 @@
+.container {
+ display: flex;
+ gap: 2rem;
+}
+
+.block {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ width: 15rem;
+}
+
+.rectangle {
+ width: 100%;
+ height: 5rem;
+}
+
+.button {
+ height: 5rem;
+}
diff --git a/hooks/use-debounced-callback/src/useDebouncedCallback.stories.tsx b/hooks/use-debounced-callback/src/useDebouncedCallback.stories.tsx
new file mode 100644
index 00000000..012dd176
--- /dev/null
+++ b/hooks/use-debounced-callback/src/useDebouncedCallback.stories.tsx
@@ -0,0 +1,47 @@
+import React, { useState } from 'react';
+import type { StoryObj } from '@storybook/react';
+import useDebouncedCallback from './useDebouncedCallback';
+import styles from './useDebouncedCallback.stories.module.css';
+
+const COLORS = ['red', 'green', 'yellow'];
+
+const DebouncedColorChange = () => {
+ const [firstColorIndex, setFirstColorIndex] = useState(0);
+ const [secondColorIndex, setSecondColorIndex] = useState(0);
+ const setDebouncedSecondColorIndex = useDebouncedCallback(setSecondColorIndex, 1000);
+
+ return (
+
+
+
setFirstColorIndex((firstColorIndex + 1) % 3)}
+ >
+ Click for color change
+
+
+
+
+
+
setDebouncedSecondColorIndex((secondColorIndex + 1) % 3)}
+ >
+ Click for debounced color change (delay: 1000 ms)
+
+
+
+
+ );
+};
+
+export const DebouncedColorChangeStory: StoryObj = {
+ name: 'Debounced color change',
+ render: DebouncedColorChange
+};
+
+export default {
+ title: 'hooks/useDebouncedCallback'
+};
diff --git a/hooks/use-debounced-callback/src/useDebouncedCallback.tests.tsx b/hooks/use-debounced-callback/src/useDebouncedCallback.tests.tsx
new file mode 100644
index 00000000..a63567c3
--- /dev/null
+++ b/hooks/use-debounced-callback/src/useDebouncedCallback.tests.tsx
@@ -0,0 +1,272 @@
+import { waitFor } from '@testing-library/react';
+import { renderHook, act } from '@testing-library/react-hooks';
+import useDebouncedCallback from './useDebouncedCallback';
+
+const oldValue = 'old value';
+const newValue = 'new value';
+
+describe('hooks/useDebouncedCallback', () => {
+ test('works correctly with one handler call', async () => {
+ let value = oldValue;
+ const handle = () => (value = newValue);
+ const { result } = renderHook(() => useDebouncedCallback(handle, 1000));
+ const setDebounceValue = result.current;
+
+ act(setDebounceValue);
+
+ expect(value).toEqual(oldValue);
+
+ await waitFor(
+ () => {
+ expect(value).toEqual(oldValue);
+ },
+ { timeout: 500 }
+ );
+
+ await waitFor(
+ () => {
+ expect(value).toEqual(newValue);
+ },
+ { timeout: 1500 }
+ );
+ });
+
+ test('works correctly with several handler calls', async () => {
+ const handle = jest.fn();
+ const { result } = renderHook(() => useDebouncedCallback(handle, 500));
+ const setDebounceValue = result.current;
+
+ act(() => {
+ setDebounceValue();
+ setDebounceValue();
+ });
+
+ await waitFor(
+ () => {
+ expect(handle).toBeCalledTimes(1);
+ },
+ { timeout: 1000 }
+ );
+
+ act(() => {
+ setDebounceValue();
+ setDebounceValue();
+ setDebounceValue();
+ });
+
+ await waitFor(
+ () => {
+ expect(handle).toBeCalledTimes(2);
+ },
+ { timeout: 1000 }
+ );
+ });
+
+ test('cleans up timer on unmount', async () => {
+ jest.spyOn(global, 'clearTimeout');
+
+ const handle = jest.fn();
+ const { result, unmount } = renderHook(() => useDebouncedCallback(handle, 500));
+ const setDebounceValue = result.current;
+
+ act(() => {
+ setDebounceValue();
+ });
+
+ unmount();
+
+ await waitFor(
+ () => {
+ expect(handle).not.toBeCalled();
+ },
+ { timeout: 600 }
+ );
+ expect(clearTimeout).toHaveBeenCalled();
+ });
+
+ test('uses the latest callback after it changes', async () => {
+ let value = '';
+
+ const { result, rerender } = renderHook(({ callback }) => useDebouncedCallback(callback, 500), {
+ initialProps: { callback: () => (value = oldValue) }
+ });
+
+ const setDebounceValue = result.current;
+
+ act(() => {
+ setDebounceValue();
+ });
+
+ rerender({ callback: () => (value = newValue) });
+
+ await waitFor(
+ () => {
+ expect(value).toEqual(newValue);
+ },
+ { timeout: 600 }
+ );
+ });
+
+ test('executes callback immediately with delay = 0', async () => {
+ const handle = jest.fn();
+ const { result } = renderHook(() => useDebouncedCallback(handle, 0));
+ const setDebounceValue = result.current;
+
+ act(() => {
+ setDebounceValue();
+ });
+
+ await waitFor(
+ () => {
+ expect(handle).toBeCalledTimes(1);
+ },
+ { timeout: 100 }
+ );
+ });
+
+ test('cancels previous timer before starting a new one', async () => {
+ const handle = jest.fn();
+ const { result } = renderHook(() => useDebouncedCallback(handle, 500));
+ const setDebounceValue = result.current;
+
+ act(() => {
+ setDebounceValue();
+ });
+
+ await waitFor(
+ () => {
+ expect(handle).not.toBeCalled();
+ },
+ { timeout: 300 }
+ );
+
+ act(() => {
+ setDebounceValue();
+ });
+
+ await waitFor(
+ () => {
+ expect(handle).toBeCalledTimes(1);
+ },
+ { timeout: 600 }
+ );
+ });
+
+ test('adjusts to updated delay value', async () => {
+ const handle = jest.fn();
+ const { result, rerender } = renderHook(({ delay }) => useDebouncedCallback(handle, delay), {
+ initialProps: { delay: 500 }
+ });
+ const setDebounceValue = result.current;
+
+ act(() => {
+ setDebounceValue();
+ });
+
+ rerender({ delay: 1000 });
+
+ act(() => {
+ setDebounceValue();
+ });
+
+ await waitFor(
+ () => {
+ expect(handle).not.toBeCalled();
+ },
+ { timeout: 750 }
+ );
+
+ await waitFor(
+ () => {
+ expect(handle).toBeCalledTimes(1);
+ },
+ { timeout: 750 }
+ );
+ });
+
+ test('ensure no unnecessary rerenders when dependency is omitted', async () => {
+ const handle = jest.fn();
+ const { result } = renderHook(() => useDebouncedCallback(handle, 500));
+
+ act(() => {
+ result.current(oldValue);
+ });
+
+ await waitFor(
+ () => {
+ expect(handle).toHaveBeenCalledWith(oldValue);
+ expect(handle).toHaveBeenCalledTimes(1);
+ },
+ { timeout: 600 }
+ );
+
+ act(() => {
+ result.current(newValue);
+ });
+
+ await waitFor(
+ () => {
+ expect(handle).toHaveBeenCalledWith(newValue);
+ expect(handle).toHaveBeenCalledTimes(2);
+ },
+ { timeout: 600 }
+ );
+ });
+
+ test('should update the debounced function when delay changes', async () => {
+ const handle = jest.fn();
+ const { result, rerender } = renderHook(({ callback, delay }) => useDebouncedCallback(callback, delay), {
+ initialProps: { callback: handle, delay: 1000 }
+ });
+
+ rerender({ delay: 500, callback: handle });
+
+ act(() => {
+ result.current(oldValue);
+ });
+
+ expect(handle).not.toHaveBeenCalled();
+
+ act(() => {
+ result.current(newValue);
+ });
+
+ await waitFor(
+ () => {
+ expect(handle).toHaveBeenCalledWith(newValue);
+ expect(handle).toHaveBeenCalledTimes(1);
+ },
+ { timeout: 600 }
+ );
+ });
+
+ test('should update the debounced function when callback changes', async () => {
+ const oldHandle = jest.fn();
+ const newHandle = jest.fn();
+
+ const { result, rerender } = renderHook(({ callback, delay }) => useDebouncedCallback(callback, delay), {
+ initialProps: { callback: oldHandle, delay: 500 }
+ });
+
+ rerender({ delay: 500, callback: newHandle });
+
+ act(() => {
+ result.current(oldValue);
+ });
+
+ expect(oldHandle).not.toHaveBeenCalled();
+
+ act(() => {
+ result.current(newValue);
+ });
+
+ await waitFor(
+ () => {
+ expect(newHandle).toHaveBeenCalledWith(newValue);
+ expect(newHandle).toHaveBeenCalledTimes(1);
+ expect(oldHandle).not.toBeCalled();
+ },
+ { timeout: 600 }
+ );
+ });
+});
diff --git a/hooks/use-debounced-callback/src/useDebouncedCallback.ts b/hooks/use-debounced-callback/src/useDebouncedCallback.ts
new file mode 100644
index 00000000..7431abd2
--- /dev/null
+++ b/hooks/use-debounced-callback/src/useDebouncedCallback.ts
@@ -0,0 +1,22 @@
+import { useCallback, useRef } from 'react';
+import { Nullable } from '@byndyusoft-ui/types';
+import useTimeout from '@byndyusoft-ui/use-timeout';
+
+export function useDebouncedCallback(
+ callback: (...args: A) => void,
+ delay: number
+): (...args: A) => void {
+ const argsRef = useRef>(null);
+
+ const { start } = useTimeout(() => argsRef.current && callback(...argsRef.current), delay);
+
+ return useCallback(
+ (...args: A): void => {
+ argsRef.current = args;
+ start();
+ },
+ [callback, delay]
+ );
+}
+
+export default useDebouncedCallback;
diff --git a/hooks/use-debounced-callback/tsconfig.build.json b/hooks/use-debounced-callback/tsconfig.build.json
new file mode 100644
index 00000000..2c767fd6
--- /dev/null
+++ b/hooks/use-debounced-callback/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["src/*.tests.ts", "src/*.stories.tsx"]
+}
diff --git a/hooks/use-debounced-callback/tsconfig.json b/hooks/use-debounced-callback/tsconfig.json
new file mode 100644
index 00000000..ad2f569e
--- /dev/null
+++ b/hooks/use-debounced-callback/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "declaration": true,
+ "declarationDir": "dist",
+ "outDir": "dist",
+ "module": "commonjs",
+ "target": "es6"
+ },
+ "include": ["src", "../../types.d.ts"]
+}
diff --git a/hooks/use-debounced-value/.npmignore b/hooks/use-debounced-value/.npmignore
new file mode 100644
index 00000000..85de9cf9
--- /dev/null
+++ b/hooks/use-debounced-value/.npmignore
@@ -0,0 +1 @@
+src
diff --git a/hooks/use-debounced-value/README.md b/hooks/use-debounced-value/README.md
new file mode 100644
index 00000000..c16c2602
--- /dev/null
+++ b/hooks/use-debounced-value/README.md
@@ -0,0 +1,38 @@
+# `@byndyusoft-ui/use-debounced-value`
+---
+
+> A React hook that uses to delay update state updates until a specified time period has passed without any further changes
+
+### Installation
+
+```
+npm i @byndyusoft-ui/use-debounced-value
+# or
+yarn add @byndyusoft-ui/use-debounced-value
+```
+
+### Usage
+
+```tsx
+() => {
+ const initalValue = '';
+ const delay = 1000;
+
+ const [debouncedValue, setDebouncedValue] = useDebouncedValue(initalValue, delay);
+
+ return (
+
+ setDebouncedValue(e.target.value)} />
+ Debounced result: {debouncedValue}
+
+ );
+}
+```
+
+### License
+
+Apache-2.0
+
+### Authors
+
+Anastasia Vasenina, Viktor Smorodin
diff --git a/hooks/use-debounced-value/package.json b/hooks/use-debounced-value/package.json
new file mode 100644
index 00000000..243c4270
--- /dev/null
+++ b/hooks/use-debounced-value/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@byndyusoft-ui/use-debounced-value",
+ "version": "0.0.0",
+ "description": "Byndyusoft UI React Hook",
+ "keywords": [
+ "byndyusoft",
+ "byndyusoft-ui",
+ "react",
+ "hook",
+ "debounce"
+ ],
+ "author": "Anastasia Vasenina ",
+ "homepage": "https://github.com/Byndyusoft/ui/tree/master/hooks/use-debounced-value#readme",
+ "license": "Apache-2.0",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Byndyusoft/ui.git"
+ },
+ "scripts": {
+ "build": "tsc --project tsconfig.build.json",
+ "clean": "rimraf dist",
+ "lint": "eslint src --config ../../eslint.config.js",
+ "test": "jest --config ../../jest.config.js --roots hooks/use-debounced-value/src"
+ },
+ "bugs": {
+ "url": "https://github.com/Byndyusoft/ui/issues"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "peerDependencies": {
+ "@byndyusoft-ui/use-debounced-callback": "*"
+ }
+}
diff --git a/hooks/use-debounced-value/src/index.ts b/hooks/use-debounced-value/src/index.ts
new file mode 100644
index 00000000..9adcf053
--- /dev/null
+++ b/hooks/use-debounced-value/src/index.ts
@@ -0,0 +1 @@
+export { default } from './useDebouncedValue';
diff --git a/hooks/use-debounced-value/src/useDebouncedValue.docs.mdx b/hooks/use-debounced-value/src/useDebouncedValue.docs.mdx
new file mode 100644
index 00000000..7e1eef18
--- /dev/null
+++ b/hooks/use-debounced-value/src/useDebouncedValue.docs.mdx
@@ -0,0 +1,19 @@
+import { Markdown, Source, Canvas, Meta } from '@storybook/blocks';
+import * as useDebouncedValueStories from './useDebouncedValue.stories';
+import Readme from '../README.md';
+
+
+
+{Readme}
+
+## Guide
+
+To use the hook in your project you must:
+
+1. Import the hook where you need it:
+
+
+
+2. Give start value and delay args:
+
+
diff --git a/hooks/use-debounced-value/src/useDebouncedValue.stories.module.css b/hooks/use-debounced-value/src/useDebouncedValue.stories.module.css
new file mode 100644
index 00000000..7de9f425
--- /dev/null
+++ b/hooks/use-debounced-value/src/useDebouncedValue.stories.module.css
@@ -0,0 +1,36 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.block {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ width: 2rem;
+}
+
+.title {
+ width: 8rem;
+}
+
+.input {
+ max-height: 3.5rem;
+ width: 12rem;
+}
+
+.divider {
+ width: 100%;
+ height: 1px;
+ margin: 0.5rem 0;
+
+ background-color: gray;
+}
diff --git a/hooks/use-debounced-value/src/useDebouncedValue.stories.tsx b/hooks/use-debounced-value/src/useDebouncedValue.stories.tsx
new file mode 100644
index 00000000..a247923d
--- /dev/null
+++ b/hooks/use-debounced-value/src/useDebouncedValue.stories.tsx
@@ -0,0 +1,52 @@
+import React, { useState } from 'react';
+import { StoryObj } from '@storybook/react';
+import useDebouncedValue from './useDebouncedValue';
+import styles from './useDebouncedValue.stories.module.css';
+
+const DebouncedInput = () => {
+ const [delay, setDelay] = useState(1000);
+ const [debouncedValue, setDebouncedValue] = useDebouncedValue('', delay);
+
+ return (
+
+
+ Delay:
+ setDelay(delay - 100)}
+ >
+ -
+
+
+ {`${delay} ms`}
+
+ setDelay(delay + 100)}>
+ +
+
+
+
+
+ Type anything:
+ setDebouncedValue(e.target.value)} />
+
+
+
+
+
+ Debounced result:
+ {debouncedValue}
+
+
+ );
+};
+
+export const DebouncedInputStory: StoryObj = {
+ name: 'Debounced input',
+ render: DebouncedInput
+};
+
+export default {
+ title: 'hooks/useDebouncedValue'
+};
diff --git a/hooks/use-debounced-value/src/useDebouncedValue.tests.tsx b/hooks/use-debounced-value/src/useDebouncedValue.tests.tsx
new file mode 100644
index 00000000..db706cfd
--- /dev/null
+++ b/hooks/use-debounced-value/src/useDebouncedValue.tests.tsx
@@ -0,0 +1,36 @@
+import { waitFor } from '@testing-library/react';
+import { renderHook, act } from '@testing-library/react-hooks';
+import useDebouncedValue from './useDebouncedValue';
+
+const oldValue = 'old value';
+const newValue = 'new value';
+
+describe('hooks/useDebouncedValue', () => {
+ test('works correctly', async () => {
+ const { result } = renderHook(() => useDebouncedValue(oldValue, 2000));
+ const getCurrentDebouncedValue = (): string => result.current[0];
+ const [, setDebouncedValue] = result.current;
+
+ expect(getCurrentDebouncedValue()).toEqual(oldValue);
+
+ act(() => {
+ setDebouncedValue(newValue);
+ });
+
+ expect(getCurrentDebouncedValue()).toEqual(oldValue);
+
+ await waitFor(
+ () => {
+ expect(getCurrentDebouncedValue()).toEqual(oldValue);
+ },
+ { timeout: 1500 }
+ );
+
+ await waitFor(
+ () => {
+ expect(getCurrentDebouncedValue()).toEqual(newValue);
+ },
+ { timeout: 2500 }
+ );
+ });
+});
diff --git a/hooks/use-debounced-value/src/useDebouncedValue.ts b/hooks/use-debounced-value/src/useDebouncedValue.ts
new file mode 100644
index 00000000..ebd5cd84
--- /dev/null
+++ b/hooks/use-debounced-value/src/useDebouncedValue.ts
@@ -0,0 +1,13 @@
+import { useMemo, useState } from 'react';
+import { InitialState } from '@byndyusoft-ui/types';
+import useDebouncedCallback from '@byndyusoft-ui/use-debounced-callback';
+
+type THookReturn = [T, (arg: T) => void];
+
+export default function useDebouncedValue(value: InitialState, delay = 300): THookReturn {
+ const [debouncedValue, setValue] = useState(value);
+
+ const setDebouncedValue = useDebouncedCallback(setValue, delay);
+
+ return useMemo(() => [debouncedValue, setDebouncedValue], [debouncedValue, setDebouncedValue]);
+}
diff --git a/hooks/use-debounced-value/tsconfig.build.json b/hooks/use-debounced-value/tsconfig.build.json
new file mode 100644
index 00000000..2c767fd6
--- /dev/null
+++ b/hooks/use-debounced-value/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["src/*.tests.ts", "src/*.stories.tsx"]
+}
diff --git a/hooks/use-debounced-value/tsconfig.json b/hooks/use-debounced-value/tsconfig.json
new file mode 100644
index 00000000..ad2f569e
--- /dev/null
+++ b/hooks/use-debounced-value/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "declaration": true,
+ "declarationDir": "dist",
+ "outDir": "dist",
+ "module": "commonjs",
+ "target": "es6"
+ },
+ "include": ["src", "../../types.d.ts"]
+}
diff --git a/package-lock.json b/package-lock.json
index 84f34f2d..23e48d23 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -122,6 +122,36 @@
"@byndyusoft-ui/use-event-listener": "*"
}
},
+ "hooks/use-debounce": {
+ "name": "@byndyusoft-ui/use-debounce",
+ "version": "0.0.0",
+ "extraneous": true,
+ "license": "Apache-2.0"
+ },
+ "hooks/use-debounce-callback": {
+ "version": "0.0.0",
+ "extraneous": true,
+ "license": "Apache-2.0"
+ },
+ "hooks/use-debounced-callback": {
+ "name": "@byndyusoft-ui/use-debounced-callback",
+ "version": "0.0.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@byndyusoft-ui/use-timeout": "*"
+ },
+ "peerDependencies": {
+ "@byndyusoft-ui/types": "*"
+ }
+ },
+ "hooks/use-debounced-value": {
+ "name": "@byndyusoft-ui/use-debounced-value",
+ "version": "0.0.0",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@byndyusoft-ui/use-debounced-callback": "*"
+ }
+ },
"hooks/use-event-listener": {
"name": "@byndyusoft-ui/use-event-listener",
"version": "0.1.2",
@@ -2442,6 +2472,14 @@
"resolved": "hooks/use-click-outside",
"link": true
},
+ "node_modules/@byndyusoft-ui/use-debounced-callback": {
+ "resolved": "hooks/use-debounced-callback",
+ "link": true
+ },
+ "node_modules/@byndyusoft-ui/use-debounced-value": {
+ "resolved": "hooks/use-debounced-value",
+ "link": true
+ },
"node_modules/@byndyusoft-ui/use-event-listener": {
"resolved": "hooks/use-event-listener",
"link": true
@@ -36904,6 +36942,16 @@
"@byndyusoft-ui/use-event-listener": "*"
}
},
+ "@byndyusoft-ui/use-debounced-callback": {
+ "version": "file:hooks/use-debounced-callback",
+ "requires": {
+ "@byndyusoft-ui/use-timeout": "*"
+ }
+ },
+ "@byndyusoft-ui/use-debounced-value": {
+ "version": "file:hooks/use-debounced-value",
+ "requires": {}
+ },
"@byndyusoft-ui/use-event-listener": {
"version": "file:hooks/use-event-listener",
"requires": {