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))} /> - +
-
+
from setFromValue(Number(e.target.value))} /> to setLessValue(Number(e.target.value))} /> - +
-
+
index setIndexUpdateValue(Number(e.target.value))} /> item setUpdateValue(Number(e.target.value))} /> - +
-
+
setIndexRemoveValue(Number(e.target.value))} /> - +
-
- - - -
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 ( +
+
+ +
+
+ +
+ +
+
+
+ ); +}; + +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: + + + {`${delay} ms`} + + +
+ +
+ 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": {