Repository: streamich/react-use
Pull request #519 (2019-08-13 23:51:30)

feat: add min/max to useNumber
Implemented #273;
Added runtime type checks;
Tests for all that stuff;
Docs;


Patch:
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) &mdash; tracks latest value of an `Observable`.
   - [`useSetState`](./docs/useSetState.md) &mdash; creates `setState` method which works like `this.setState`. [![][img-demo]](https://codesandbox.io/s/n75zqn1xp0)
   - [`useToggle` and `useBoolean`](./docs/useToggle.md) &mdash; tracks state of a boolean.
-  - [`useCounter` and `useNumber`](./docs/useCounter.md) &mdash; tracks state of a number.
+  - [`useCounter` and `useNumber`](./docs/useCounter.md) &mdash; tracks state of a number. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usecounter--demo)
   - [`useList`](./docs/useList.md) &mdash; tracks state of an array.
   - [`useMap`](./docs/useMap.md) &mdash; 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 (
     <div>
-      <div>{value} is {get()}</div>
-      <button onClick={() => inc()}>Increment</button>
-      <button onClick={() => dec()}>Decrement</button>
-      <button onClick={() => inc(5)}>Increment (+5)</button>
-      <button onClick={() => dec(5)}>Decrement (-5)</button>
-      <button onClick={() => set(100)}>Set 100</button>
-      <button onClick={() => reset()}>Reset</button>
-      <button onClick={() => reset(25)}>Reset (25)</button>
+      <div>
+        current: { value } [min: { min }; max: { max }]
+      </div>
+
+      <br />
+      Current value: <button onClick={ () => inc() }>Increment</button>
+      <button onClick={ () => dec() }>Decrement</button>
+      <button onClick={ () => inc(5) }>Increment (+5)</button>
+      <button onClick={ () => dec(5) }>Decrement (-5)</button>
+      <button onClick={ () => set(100) }>Set 100</button>
+      <button onClick={ () => reset() }>Reset</button>
+      <button onClick={ () => reset(25) }>Reset (25)</button>
+
+      <br />
+      <br />
+      Min value:
+      <button onClick={ () => incMin() }>Increment</button>
+      <button onClick={ () => decMin() }>Decrement</button>
+
+      <br />
+      <br />
+      Max value:
+      <button onClick={ () => incMax() }>Increment</button>
+      <button onClick={ () => decMax() }>Decrement</button>
     </div>
   );
 };
 ```
+
+
+## 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 (
     <div>
       <div>
-        {value} is {get()}
+        current: {value} [min: {min}; max: {max}]
       </div>
-      <button onClick={() => inc()}>Increment</button>
+      <br />
+      Current value: <button onClick={() => inc()}>Increment</button>
       <button onClick={() => dec()}>Decrement</button>
       <button onClick={() => inc(5)}>Increment (+5)</button>
       <button onClick={() => dec(5)}>Decrement (-5)</button>
       <button onClick={() => set(100)}>Set 100</button>
       <button onClick={() => reset()}>Reset</button>
       <button onClick={() => reset(25)}>Reset (25)</button>
+      <br />
+      <br />
+      Min value:
+      <button onClick={() => incMin()}>Increment</button>
+      <button onClick={() => decMin()}>Decrement</button>
+      <br />
+      <br />
+      Max value:
+      <button onClick={() => incMax()}>Increment</button>
+      <button onClick={() => decMax()}>Decrement</button>
     </div>
   );
 };
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<number>(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<number>(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;
+}


Test patch:
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();
+  });
+});