diff --git a/README.md b/README.md
index 9e97e95b..d24873e0 100644
--- a/README.md
+++ b/README.md
@@ -127,7 +127,7 @@
- [`useObservable`](./docs/useObservable.md) — tracks latest value of an `Observable`.
- [`useSetState`](./docs/useSetState.md) — creates `setState` method which works like `this.setState`. [![][img-demo]](https://codesandbox.io/s/n75zqn1xp0)
- [`useToggle` and `useBoolean`](./docs/useToggle.md) — tracks state of a boolean.
- - [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number.
+ - [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usecounter--demo)
- [`useList`](./docs/useList.md) — tracks state of an array.
- [`useMap`](./docs/useMap.md) — tracks state of an object.
diff --git a/docs/useCounter.md b/docs/useCounter.md
index a64102a8..72146696 100644
--- a/docs/useCounter.md
+++ b/docs/useCounter.md
@@ -11,19 +11,50 @@ React state hook that tracks a numeric value.
import {useCounter, useNumber} from 'react-use';
const Demo = () => {
- const [value, {inc, dec, get, set, reset}] = useCounter(5);
+ const [min, { inc: incMin, dec: decMin }] = useCounter(1);
+ const [max, { inc: incMax, dec: decMax }] = useCounter(10);
+ const [value, { inc, dec, set, reset }] = useCounter(5, max, min);
return (
-
{value} is {get()}
-
-
-
-
-
-
-
+
+ current: { value } [min: { min }; max: { max }]
+
+
+
+ Current value:
+
+
+
+
+
+
+
+
+
+ Min value:
+
+
+
+
+
+ Max value:
+
+
);
};
```
+
+
+## Reference
+
+```ts
+const [ current, { inc, dec, get, set, reset } ] = useCounter(initial: number, max: number | null = null, 20: number | null = null);
+```
+- `current` - current counter value;
+- `get(): number` - getter of current counter value;
+- `inc(delta: number): void` - increment current value;
+- `dec(delta: number): void` - decrement current value;
+- `set(value: number): void` - set arbitrary value;
+- `reset(value: number): void` - as the `set`, but also will assign value by reference to the `initial` parameter;
diff --git a/src/__stories__/useCounter.story.tsx b/src/__stories__/useCounter.story.tsx
index e8c98c59..8c64f8db 100644
--- a/src/__stories__/useCounter.story.tsx
+++ b/src/__stories__/useCounter.story.tsx
@@ -4,20 +4,33 @@ import { useCounter } from '..';
import ShowDocs from './util/ShowDocs';
const Demo = () => {
- const [value, { inc, dec, get, set, reset }] = useCounter(5);
+ const [min, { inc: incMin, dec: decMin }] = useCounter(1);
+ const [max, { inc: incMax, dec: decMax }] = useCounter(10);
+ const [value, { inc, dec, set, reset }] = useCounter(5, max, min);
return (
- {value} is {get()}
+ current: {value} [min: {min}; max: {max}]
-
+
+ Current value:
+
+
+ Min value:
+
+
+
+
+ Max value:
+
+
);
};
diff --git a/src/__tests__/useCounter.test.ts b/src/__tests__/useCounter.test.ts
index 41cc1e22..766243ce 100644
--- a/src/__tests__/useCounter.test.ts
+++ b/src/__tests__/useCounter.test.ts
@@ -1,7 +1,8 @@
import { act, renderHook } from '@testing-library/react-hooks';
import useCounter from '../useCounter';
-const setUp = (initialValue?: number) => renderHook(() => useCounter(initialValue));
+const setUp = (initialValue?: number, max: number | null = null, min: number | null = null) =>
+ renderHook(() => useCounter(initialValue, max, min));
it('should init counter and utils', () => {
const { result } = setUp(5);
@@ -121,8 +122,81 @@ it('should reset and set new original value', () => {
expect(get()).toBe(8);
});
-it.todo('should log an error if initial value is other than a number');
-it.todo('should log an error if increment value is other than a number');
-it.todo('should log an error if increment value is a negative number');
-it.todo('should log an error if decrement value is other than a number');
-it.todo('should log an error if decrement value is a negative number');
+it('should not exceed max value', () => {
+ const { result } = setUp(10, 5);
+ expect(result.current[0]).toBe(5);
+
+ const { get, inc, reset } = result.current[1];
+
+ act(() => reset(10));
+ expect(get()).toBe(5);
+
+ act(() => reset(4));
+ expect(get()).toBe(4);
+
+ act(() => inc());
+ expect(get()).toBe(5);
+
+ act(() => inc());
+ expect(get()).toBe(5);
+});
+
+it('should not exceed min value', () => {
+ const { result } = setUp(3, null, 5);
+ expect(result.current[0]).toBe(5);
+
+ const { get, dec, reset } = result.current[1];
+
+ act(() => reset(4));
+ expect(get()).toBe(5);
+
+ act(() => reset(6));
+ expect(get()).toBe(6);
+
+ act(() => dec());
+ expect(get()).toBe(5);
+
+ act(() => dec());
+ expect(get()).toBe(5);
+});
+
+describe('should `console.error` on unexpected inputs', () => {
+ it('on any of call parameters', () => {
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ // @ts-ignore
+ setUp(false);
+ expect(spy.mock.calls[0][0]).toBe('initialValue has to be a number, got boolean');
+
+ // @ts-ignore
+ setUp(10, false);
+ expect(spy.mock.calls[1][0]).toBe('max has to be a number, got boolean');
+
+ // @ts-ignore
+ setUp(10, 5, {});
+ expect(spy.mock.calls[2][0]).toBe('min has to be a number, got object');
+
+ spy.mockRestore();
+ });
+
+ it('on any of returned methods has unexpected input', () => {
+ const { result } = setUp(10);
+ const { inc, dec, reset } = result.current[1];
+
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ // @ts-ignore
+ act(() => inc(false));
+ expect(spy.mock.calls[0][0]).toBe('delta has to be a number, got boolean');
+
+ // @ts-ignore
+ act(() => dec(false));
+ expect(spy.mock.calls[1][0]).toBe('delta has to be a number, got boolean');
+
+ // @ts-ignore
+ act(() => reset({}));
+ expect(spy.mock.calls[2][0]).toBe('value has to be a number, got object');
+
+ spy.mockRestore();
+ });
+});
diff --git a/src/useCounter.ts b/src/useCounter.ts
index 19243ced..0c3fbae0 100644
--- a/src/useCounter.ts
+++ b/src/useCounter.ts
@@ -9,14 +9,70 @@ export interface CounterActions {
reset: (value?: number) => void;
}
-const useCounter = (initialValue: number = 0): [number, CounterActions] => {
- const [get, set] = useGetSet(initialValue);
- const inc = useCallback((delta: number = 1) => set(get() + delta), []);
- const dec = useCallback((delta: number = 1) => inc(-delta), []);
- const reset = useCallback((value: number = initialValue) => {
- initialValue = value;
- set(value);
- }, []);
+export default function useCounter(
+ initialValue: number = 0,
+ max: number | null = null,
+ min: number | null = null
+): [number, CounterActions] {
+ typeof initialValue !== 'number' && console.error('initialValue has to be a number, got ' + typeof initialValue);
+
+ if (typeof min === 'number') {
+ initialValue = Math.max(initialValue, min);
+ } else if (min !== null) {
+ console.error('min has to be a number, got ' + typeof min);
+ }
+
+ if (typeof max === 'number') {
+ initialValue = Math.min(initialValue, max);
+ } else if (max !== null) {
+ console.error('max has to be a number, got ' + typeof max);
+ }
+
+ const [get, setInternal] = useGetSet(initialValue);
+
+ function set(value: number): void {
+ const current = get();
+
+ if (current === value) {
+ return;
+ }
+
+ if (typeof min === 'number') {
+ value = Math.max(value, min);
+ }
+ if (typeof max === 'number') {
+ value = Math.min(value, max);
+ }
+
+ current !== value && setInternal(value);
+ }
+
+ const inc = useCallback(
+ (delta: number = 1) => {
+ typeof delta !== 'number' && console.error('delta has to be a number, got ' + typeof delta);
+
+ set(get() + delta);
+ },
+ [max, min]
+ );
+ const dec = useCallback(
+ (delta: number = 1) => {
+ typeof delta !== 'number' && console.error('delta has to be a number, got ' + typeof delta);
+
+ set(get() - delta);
+ },
+ [max, min]
+ );
+ const reset = useCallback(
+ (value: number = initialValue) => {
+ typeof value !== 'number' && console.error('value has to be a number, got ' + typeof value);
+
+ initialValue = value;
+ set(value);
+ },
+ [max, min]
+ );
+
const actions = {
inc,
dec,
@@ -26,6 +82,4 @@ const useCounter = (initialValue: number = 0): [number, CounterActions] => {
};
return [get(), actions];
-};
-
-export default useCounter;
+}