diff --git a/src/__tests__/useCounter.test.ts b/src/__tests__/useCounter.test.ts index 766243cefe..1eb8734f78 100644 --- a/src/__tests__/useCounter.test.ts +++ b/src/__tests__/useCounter.test.ts @@ -187,15 +187,15 @@ describe('should `console.error` on unexpected inputs', () => { // @ts-ignore act(() => inc(false)); - expect(spy.mock.calls[0][0]).toBe('delta has to be a number, got boolean'); + expect(spy.mock.calls[0][0]).toBe('delta has to be a number or function returning a number, got boolean'); // @ts-ignore act(() => dec(false)); - expect(spy.mock.calls[1][0]).toBe('delta has to be a number, got boolean'); + expect(spy.mock.calls[1][0]).toBe('delta has to be a number or function returning a number, got boolean'); // @ts-ignore act(() => reset({})); - expect(spy.mock.calls[2][0]).toBe('value has to be a number, got object'); + expect(spy.mock.calls[2][0]).toBe('value has to be a number or function returning a number, got object'); spy.mockRestore(); }); diff --git a/src/useCounter.ts b/src/useCounter.ts index 0c3fbae014..649829f966 100644 --- a/src/useCounter.ts +++ b/src/useCounter.ts @@ -1,85 +1,89 @@ -import { useCallback } from 'react'; +import { useMemo } from 'react'; import useGetSet from './useGetSet'; +import { HookState, InitialHookState, resolveHookState } from './util/resolveHookState'; export interface CounterActions { inc: (delta?: number) => void; dec: (delta?: number) => void; get: () => number; - set: (value: number) => void; - reset: (value?: number) => void; + set: (value: HookState) => void; + reset: (value?: HookState) => void; } export default function useCounter( - initialValue: number = 0, + initialValue: InitialHookState = 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); + let init = resolveHookState(initialValue); + + typeof init !== 'number' && console.error('initialValue has to be a number, got ' + typeof initialValue); if (typeof min === 'number') { - initialValue = Math.max(initialValue, min); + init = Math.max(init, 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); + init = Math.min(init, max); } else if (max !== null) { console.error('max has to be a number, got ' + typeof max); } - const [get, setInternal] = useGetSet(initialValue); + const [get, setInternal] = useGetSet(init); - function set(value: number): void { - const current = get(); + return [ + get(), + useMemo(() => { + const set = (newState: HookState) => { + const prevState = get(); + let rState = resolveHookState(newState, prevState); - if (current === value) { - return; - } + if (prevState !== rState) { + if (typeof min === 'number') { + rState = Math.max(rState, min); + } + if (typeof max === 'number') { + rState = Math.min(rState, max); + } - if (typeof min === 'number') { - value = Math.max(value, min); - } - if (typeof max === 'number') { - value = Math.min(value, max); - } + prevState !== rState && setInternal(rState); + } + }; - current !== value && setInternal(value); - } + return { + get, + set, + inc: (delta: HookState = 1) => { + const rDelta = resolveHookState(delta, get()); + + if (typeof rDelta !== 'number') { + console.error('delta has to be a number or function returning a number, got ' + typeof rDelta); + } + + set((num: number) => num + rDelta); + }, + dec: (delta: HookState = 1) => { + const rDelta = resolveHookState(delta, get()); + + if (typeof rDelta !== 'number') { + console.error('delta has to be a number or function returning a number, got ' + typeof rDelta); + } + + set((num: number) => num - rDelta); + }, + reset: (value: HookState = init) => { + const rValue = resolveHookState(value, get()); + + if (typeof rValue !== 'number') { + console.error('value has to be a number or function returning a number, got ' + typeof rValue); + } - 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, - get, - set, - reset, - }; - - return [get(), actions]; + init = rValue; + set(rValue); + }, + }; + }, [min, max]), + ]; } diff --git a/src/util/resolveHookState.ts b/src/util/resolveHookState.ts index a207afc99f..18d39ee40e 100644 --- a/src/util/resolveHookState.ts +++ b/src/util/resolveHookState.ts @@ -5,14 +5,12 @@ export type InitialHookState = S | InitialStateSetter; export type HookState = S | StateSetter; export type ResolvableHookState = S | StateSetter | InitialStateSetter; -export function resolveHookState(newState: S | InitialStateSetter): S; -export function resolveHookState(newState: Exclude, StateSetter>, currentState: S): S; -// tslint:disable-next-line:unified-signatures -export function resolveHookState(newState: StateSetter, currentState: S): S; -export function resolveHookState(newState: ResolvableHookState, currentState?: S): S { +export function resolveHookState(newState: StateSetter, currentState: C): S; +export function resolveHookState(newState: ResolvableHookState, currentState?: C): S; +export function resolveHookState(newState: ResolvableHookState, currentState?: C): S { if (typeof newState === 'function') { return (newState as Function)(currentState); } - return newState as S; + return newState; }