From 1628e571756d175f3a940211ad556df3c0f3f882 Mon Sep 17 00:00:00 2001 From: xobotyi Date: Wed, 14 Aug 2019 02:49:53 +0300 Subject: [PATCH] Implemented #273; Added runtime type checks; Tests for all that stuff; Docs; --- README.md | 2 +- docs/useCounter.md | 49 +++++++++++++--- src/__stories__/useCounter.story.tsx | 19 +++++- src/__tests__/useCounter.test.ts | 86 ++++++++++++++++++++++++++-- src/useCounter.ts | 76 ++++++++++++++++++++---- 5 files changed, 202 insertions(+), 30 deletions(-) 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; +}