From 9fd02eb821c670c9f1684dca9a652d7cdd11a870 Mon Sep 17 00:00:00 2001 From: xobotyi Date: Wed, 30 Oct 2019 16:51:33 +0300 Subject: [PATCH 1/3] feat: react-like state resolver to use it in stateful hooks; --- src/__tests__/resolveHookState.ts | 26 ++++++++++++++++++++++++++ src/util/resolveHookState.ts | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/__tests__/resolveHookState.ts create mode 100644 src/util/resolveHookState.ts diff --git a/src/__tests__/resolveHookState.ts b/src/__tests__/resolveHookState.ts new file mode 100644 index 00000000..520136bf --- /dev/null +++ b/src/__tests__/resolveHookState.ts @@ -0,0 +1,26 @@ +import { resolveHookState } from '../util/resolveHookState'; + +describe('resolveHookState', () => { + it('should defined', () => { + expect(resolveHookState).toBeDefined(); + }); + + it(`should return value as is if it's not a function`, () => { + expect(resolveHookState(1)).toBe(1); + expect(resolveHookState('HI!')).toBe('HI!'); + expect(resolveHookState(undefined)).toBe(undefined); + }); + + it('should call passed function', () => { + const spy = jest.fn(); + resolveHookState(spy); + expect(spy).toHaveBeenCalled(); + }); + + it('should pass 2nd parameter to function', () => { + const spy = jest.fn(); + resolveHookState(spy, 123); + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls[0][0]).toBe(123); + }); +}); diff --git a/src/util/resolveHookState.ts b/src/util/resolveHookState.ts new file mode 100644 index 00000000..a207afc9 --- /dev/null +++ b/src/util/resolveHookState.ts @@ -0,0 +1,18 @@ +export type StateSetter = (prevState: S) => S; +export type InitialStateSetter = () => S; + +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 { + if (typeof newState === 'function') { + return (newState as Function)(currentState); + } + + return newState as S; +} From 9b5d0f2ad13bb03f87acad54b7faab3d2ea0b9ce Mon Sep 17 00:00:00 2001 From: xobotyi Date: Thu, 31 Oct 2019 02:58:48 +0300 Subject: [PATCH 2/3] feat(useGetSet): reworked with use of new resolveHookState function plus improved memory usage; --- src/useGetSet.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/useGetSet.ts b/src/useGetSet.ts index 33768aea..9cb4e021 100644 --- a/src/useGetSet.ts +++ b/src/useGetSet.ts @@ -1,16 +1,21 @@ -import { useCallback, useRef } from 'react'; +import { Dispatch, useMemo, useRef } from 'react'; import useUpdate from './useUpdate'; +import { HookState, InitialHookState, resolveHookState } from './util/resolveHookState'; -const useGetSet = (initialValue: T): [() => T, (value: T) => void] => { - const state = useRef(initialValue); +export default function useGetSet(initialState: InitialHookState): [() => S, Dispatch>] { + const state = useRef(resolveHookState(initialState)); const update = useUpdate(); - const get = useCallback(() => state.current, []); - const set = useCallback((value: T) => { - state.current = value; - update(); - }, []); - return [get, set]; -}; - -export default useGetSet; + return useMemo( + () => [ + // get + () => state.current as S, + // set + (newState: HookState) => { + state.current = resolveHookState(newState, state.current); + update(); + }, + ], + [] + ); +} From befcf84c2c71a9ff3c83c83c0afe3e733737add3 Mon Sep 17 00:00:00 2001 From: xobotyi Date: Thu, 31 Oct 2019 04:48:30 +0300 Subject: [PATCH 3/3] feat(useCounter): reworked with use of new resolveHookState function plus improved memory usage; feat(resolveHookState): improved types; --- src/__tests__/useCounter.test.ts | 6 +- src/useCounter.ts | 106 ++++++++++++++++--------------- src/util/resolveHookState.ts | 10 ++- 3 files changed, 62 insertions(+), 60 deletions(-) diff --git a/src/__tests__/useCounter.test.ts b/src/__tests__/useCounter.test.ts index 766243ce..1eb8734f 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 0c3fbae0..649829f9 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()); - const inc = useCallback( - (delta: number = 1) => { - typeof delta !== 'number' && console.error('delta has to be a number, got ' + typeof delta); + if (typeof rDelta !== 'number') { + console.error('delta has to be a number or function returning a number, got ' + typeof rDelta); + } - 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((num: number) => num + rDelta); + }, + dec: (delta: HookState = 1) => { + const rDelta = resolveHookState(delta, get()); - 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); + if (typeof rDelta !== 'number') { + console.error('delta has to be a number or function returning a number, got ' + typeof rDelta); + } - initialValue = value; - set(value); - }, - [max, min] - ); + set((num: number) => num - rDelta); + }, + reset: (value: HookState = init) => { + const rValue = resolveHookState(value, get()); - const actions = { - inc, - dec, - get, - set, - reset, - }; + if (typeof rValue !== 'number') { + console.error('value has to be a number or function returning a number, got ' + typeof rValue); + } - return [get(), actions]; + init = rValue; + set(rValue); + }, + }; + }, [min, max]), + ]; } diff --git a/src/util/resolveHookState.ts b/src/util/resolveHookState.ts index a207afc9..18d39ee4 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; }