diff --git a/README.md b/README.md
index a8de55da..f42ee510 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,8 @@ import { useMountEffect } from "@react-hookz/web/esnext";
— Run effect only when component first-mounted.
- [**`useRerender`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-usererender)
— Return callback that re-renders component.
+ - [**`useThrottledEffect`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-usethrottledeffect)
+ — Like `useEffect`, but passed function is throttled.
- [**`useUnmountEffect`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-useunmounteffect)
— Run effect only when component unmounted.
- [**`useUpdateEffect`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-useupdateeffect)
@@ -102,6 +104,8 @@ import { useMountEffect } from "@react-hookz/web/esnext";
— Like `useState`, but its state setter is guarded against sets on unmounted component.
- [**`useToggle`**](https://react-hookz.github.io/web/?path=/docs/state-usetoggle)
— Like `useState`, but can only become `true` or `false`.
+ - [**`useThrottledState`**](https://react-hookz.github.io/web/?path=/docs/state-usethrottledstate)
+ — Like `useSafeState` but its state setter is throttled.
- [**`useValidator`**](https://react-hookz.github.io/web/?path=/docs/state-usevalidator)
— Performs validation when any of provided dependencies has changed.
diff --git a/src/index.ts b/src/index.ts
index a6bc9e62..98da5ae8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,19 +12,23 @@ export {
useConditionalUpdateEffect,
IUseConditionalUpdateEffectPredicate,
} from './useConditionalUpdateEffect/useConditionalUpdateEffect';
+export { useDebouncedEffect } from './useDebouncedEffect/useDebouncedEffect';
export { useFirstMountState } from './useFirstMountState/useFirstMountState';
export { useIsMounted } from './useIsMounted/useIsMounted';
export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
export { useMountEffect } from './useMountEffect/useMountEffect';
export { useRerender } from './useRerender/useRerender';
+export { useThrottledEffect } from './useThrottledEffect/useThrottledEffect';
export { useUnmountEffect } from './useUnmountEffect/useUnmountEffect';
export { useUpdateEffect } from './useUpdateEffect/useUpdateEffect';
// State
+export { useDebouncedState } from './useDebouncedState/useDebouncedState';
export { useMediatedState } from './useMediatedState/useMediatedState';
export { usePrevious } from './usePrevious/usePrevious';
export { useSafeState } from './useSafeState/useSafeState';
export { useToggle } from './useToggle/useToggle';
+export { useThrottledState } from './useThrottledState/useThrottledState';
export {
useValidator,
IValidatorImmediate,
@@ -68,7 +72,3 @@ export { useMediaQuery } from './useMediaQuery/useMediaQuery';
// Dom
export { useDocumentTitle, IUseDocumentTitleOptions } from './useDocumentTitle/useDocumentTitle';
-
-export { useDebouncedEffect } from './useDebouncedEffect/useDebouncedEffect';
-
-export { useDebouncedState } from './useDebouncedState/useDebouncedState';
diff --git a/src/useDebouncedState/__docs__/example.stories.tsx b/src/useDebouncedState/__docs__/example.stories.tsx
index d32b7614..891162e6 100644
--- a/src/useDebouncedState/__docs__/example.stories.tsx
+++ b/src/useDebouncedState/__docs__/example.stories.tsx
@@ -6,7 +6,7 @@ export const Example: React.FC = () => {
return (
-
Below state will update 200ms after last change, but at least once every 500ms
+
Below state will update 300ms after last change, but at least once every 500ms
The input`s value is: {state}
(
): IThrottledFunction;
```
-- **cb** _`(...args: T) => unknown`_ - function that will be throttled.
+#### Arguments
+
+- **callback** _`(...args: T) => unknown`_ - function that will be throttled.
- **deps** _`React.DependencyList`_ - dependencies list when to update callback.
- **delay** _`number`_ - throttle delay.
-- **noTrailing** _`boolean`_ _(default: false)_ - if noTrailing is true, callback will only execute
+- **noTrailing** _`boolean`_ _(default: false)_ - if `noTrailing` is true, callback will only execute
every `delay` milliseconds, otherwise, callback will be executed once, after the last call.
diff --git a/src/useThrottledCallback/useThrottledCallback.ts b/src/useThrottledCallback/useThrottledCallback.ts
index d015ae94..3410cc83 100644
--- a/src/useThrottledCallback/useThrottledCallback.ts
+++ b/src/useThrottledCallback/useThrottledCallback.ts
@@ -12,7 +12,7 @@ export interface IThrottledFunction {
* @param callback Function that will be throttled.
* @param deps Dependencies list when to update callback.
* @param delay Throttle delay.
- * @param noTrailing If noTrailing is true, callback will only execute every
+ * @param noTrailing If `noTrailing` is true, callback will only execute every
* `delay` milliseconds, otherwise, callback will be executed one final time
* after the last throttled-function call.
*/
diff --git a/src/useThrottledEffect/__docs__/example.stories.tsx b/src/useThrottledEffect/__docs__/example.stories.tsx
new file mode 100644
index 00000000..de17a843
--- /dev/null
+++ b/src/useThrottledEffect/__docs__/example.stories.tsx
@@ -0,0 +1,33 @@
+import * as React from 'react';
+import { useState } from 'react';
+import { useThrottledEffect } from '../..';
+
+const HAS_DIGIT_REGEX = /[\d]/g;
+
+export const Example: React.FC = () => {
+ const [state, setState] = useState('');
+ const [hasNumbers, setHasNumbers] = useState(false);
+
+ useThrottledEffect(
+ () => {
+ setHasNumbers(HAS_DIGIT_REGEX.test(state));
+ },
+ [state],
+ 200
+ );
+
+ return (
+
+
Digit check will be performed no more than once every 200ms
+
+
{hasNumbers ? 'Input has digits' : 'No digits found in input'}
+ {
+ setState(ev.target.value);
+ }}
+ />
+
+ );
+};
diff --git a/src/useThrottledEffect/__docs__/story.mdx b/src/useThrottledEffect/__docs__/story.mdx
new file mode 100644
index 00000000..7ae21091
--- /dev/null
+++ b/src/useThrottledEffect/__docs__/story.mdx
@@ -0,0 +1,35 @@
+import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
+import { Example } from "./example.stories";
+
+
+
+# useThrottledEffect
+
+Like `useEffect`, but passed function is throttled.
+
+#### Example
+
+
+
+## Reference
+
+```ts
+export function useThrottledEffect(
+ callback: (...args: any[]) => void,
+ deps: DependencyList,
+ delay: number,
+ noTrailing = false
+): void;
+```
+
+#### Arguments
+
+- **callback** _`(...args: any[]) => void`_ - Callback like for `useEffect`, but without ability to
+ return a cleanup function.
+- **deps** _`DependencyList`_ - Dependencies list that will be passed to underlying `useEffect` and
+ `useThrottledCallback`.
+- **delay** _`number`_ - Throttle delay.
+- **noTrailing** _`boolean`_ _(default: false)_ - If `noTrailing` is true, callback will only execute
+ every `delay` milliseconds, otherwise, callback will be executed once, after the last call.
diff --git a/src/useThrottledEffect/__tests__/dom.ts b/src/useThrottledEffect/__tests__/dom.ts
new file mode 100644
index 00000000..70d8402e
--- /dev/null
+++ b/src/useThrottledEffect/__tests__/dom.ts
@@ -0,0 +1,39 @@
+import { renderHook } from '@testing-library/react-hooks/dom';
+import { useThrottledEffect } from '../..';
+
+describe('useThrottledEffect', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('should be defined', () => {
+ expect(useThrottledEffect).toBeDefined();
+ });
+
+ it('should render', () => {
+ const { result } = renderHook(() => useThrottledEffect(() => {}, [], 200));
+ expect(result.error).toBeUndefined();
+ });
+
+ it('should throttle passed callback', () => {
+ const spy = jest.fn();
+ const { rerender } = renderHook((dep) => useThrottledEffect(spy, [dep], 200, true), {
+ initialProps: 1,
+ });
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ rerender(2);
+ rerender(3);
+ rerender(4);
+ expect(spy).toHaveBeenCalledTimes(1);
+
+ jest.advanceTimersByTime(200);
+ expect(spy).toHaveBeenCalledTimes(1);
+ rerender(5);
+ expect(spy).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/src/useThrottledEffect/__tests__/ssr.ts b/src/useThrottledEffect/__tests__/ssr.ts
new file mode 100644
index 00000000..cab21284
--- /dev/null
+++ b/src/useThrottledEffect/__tests__/ssr.ts
@@ -0,0 +1,21 @@
+import { renderHook } from '@testing-library/react-hooks/server';
+import { useThrottledEffect } from '../..';
+
+describe('useThrottledEffect', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('should be defined', () => {
+ expect(useThrottledEffect).toBeDefined();
+ });
+
+ it('should render', () => {
+ const { result } = renderHook(() => useThrottledEffect(() => {}, [], 200));
+ expect(result.error).toBeUndefined();
+ });
+});
diff --git a/src/useThrottledEffect/useThrottledEffect.ts b/src/useThrottledEffect/useThrottledEffect.ts
new file mode 100644
index 00000000..018aa698
--- /dev/null
+++ b/src/useThrottledEffect/useThrottledEffect.ts
@@ -0,0 +1,25 @@
+import { DependencyList, useEffect } from 'react';
+import { useThrottledCallback } from '..';
+
+/**
+ * Like `useEffect`, but passed function is throttled.
+ *
+ * @param callback Callback like for `useEffect`, but without ability to return
+ * a cleanup function.
+ * @param deps Dependencies list that will be passed to underlying `useEffect`
+ * and `useThrottledCallback`.
+ * @param delay Throttle delay.
+ * @param noTrailing If `noTrailing` is true, callback will only execute every
+ * `delay` milliseconds, otherwise, callback will be executed one final time
+ * after the last throttled-function call.
+ */
+export function useThrottledEffect(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ callback: (...args: any[]) => void,
+ deps: DependencyList,
+ delay: number,
+ noTrailing = false
+): void {
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ useEffect(useThrottledCallback(callback, deps, delay, noTrailing), deps);
+}
diff --git a/src/useThrottledState/__docs__/example.stories.tsx b/src/useThrottledState/__docs__/example.stories.tsx
new file mode 100644
index 00000000..7c461bf6
--- /dev/null
+++ b/src/useThrottledState/__docs__/example.stories.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { useThrottledState } from '../..';
+
+export const Example: React.FC = () => {
+ const [state, setState] = useThrottledState('', 500);
+
+ return (
+
+
Below state will update no more than once every 500ms
+
+
The input`s value is: {state}
+ setState(ev.target.value)} />
+
+ );
+};
diff --git a/src/useThrottledState/__docs__/story.mdx b/src/useThrottledState/__docs__/story.mdx
new file mode 100644
index 00000000..17433e88
--- /dev/null
+++ b/src/useThrottledState/__docs__/story.mdx
@@ -0,0 +1,37 @@
+import {Canvas, Meta, Story} from '@storybook/addon-docs/blocks';
+import {Example} from './example.stories';
+
+
+
+# useThrottledState
+
+Lise `useSafeState` but its state setter is throttled.
+
+#### Example
+
+
+
+## Reference
+
+```ts
+export function useThrottledState(
+ initialState: S | (() => S),
+ delay: number,
+ noTrailing = false
+): [S, Dispatch>];
+```
+
+#### Arguments
+
+- **initialState** _`S | (() => S)`_ - Initial state to pass to underlying `useSafeState`.
+- **delay** _`number`_ - Throttle delay.
+- **noTrailing** _`boolean`_ _(default: false)_ - If `noTrailing` is true, callback will only execute
+every `delay` milliseconds, otherwise, callback will be executed once, after the last call.
+
+#### Return
+
+0. **state** - current state.
+1. **setState** - throttled state setter.
+
diff --git a/src/useThrottledState/__tests__/dom.ts b/src/useThrottledState/__tests__/dom.ts
new file mode 100644
index 00000000..3aab231d
--- /dev/null
+++ b/src/useThrottledState/__tests__/dom.ts
@@ -0,0 +1,40 @@
+import { renderHook, act } from '@testing-library/react-hooks/dom';
+import { useThrottledState } from '../..';
+
+describe('useThrottledState', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('should be defined', () => {
+ expect(useThrottledState).toBeDefined();
+ });
+
+ it('should render', () => {
+ const { result } = renderHook(() => useThrottledState('', 200));
+ expect(result.error).toBeUndefined();
+ });
+
+ it('should throttle set state', () => {
+ const { result } = renderHook(() => useThrottledState('', 200, true));
+
+ expect(result.current[0]).toBe('');
+ act(() => {
+ result.current[1]('hello world!');
+ });
+ expect(result.current[0]).toBe('hello world!');
+
+ result.current[1]('foo');
+ result.current[1]('bar');
+ expect(result.current[0]).toBe('hello world!');
+ jest.advanceTimersByTime(200);
+ act(() => {
+ result.current[1]('baz');
+ });
+ expect(result.current[0]).toBe('baz');
+ });
+});
diff --git a/src/useThrottledState/__tests__/ssr.ts b/src/useThrottledState/__tests__/ssr.ts
new file mode 100644
index 00000000..8648cabe
--- /dev/null
+++ b/src/useThrottledState/__tests__/ssr.ts
@@ -0,0 +1,21 @@
+import { renderHook } from '@testing-library/react-hooks/server';
+import { useThrottledState } from '../..';
+
+describe('useThrottledState', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('should be defined', () => {
+ expect(useThrottledState).toBeDefined();
+ });
+
+ it('should render', () => {
+ const { result } = renderHook(() => useThrottledState('', 200));
+ expect(result.error).toBeUndefined();
+ });
+});
diff --git a/src/useThrottledState/useThrottledState.ts b/src/useThrottledState/useThrottledState.ts
new file mode 100644
index 00000000..9e79e7da
--- /dev/null
+++ b/src/useThrottledState/useThrottledState.ts
@@ -0,0 +1,21 @@
+import { Dispatch, SetStateAction } from 'react';
+import { useSafeState, useThrottledCallback } from '..';
+
+/**
+ * Like `useSafeState` but its state setter is throttled.
+ *
+ * @param initialState Initial state to pass to underlying `useSafeState`.
+ * @param delay Throttle delay.
+ * @param noTrailing If `noTrailing` is true, callback will only execute every
+ * `delay` milliseconds, otherwise, callback will be executed one final time
+ * after the last throttled-function call.
+ */
+export function useThrottledState(
+ initialState: S | (() => S),
+ delay: number,
+ noTrailing = false
+): [S, Dispatch>] {
+ const [state, setState] = useSafeState(initialState);
+
+ return [state, useThrottledCallback(setState, [], delay, noTrailing)];
+}