Merge pull request #723 from streamich/resolveHookState-implementation

feat: react-like state resolver
This commit is contained in:
Vadim Dalecky 2019-10-31 22:01:35 +01:00 committed by GitHub
commit b8b49b3a60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 117 additions and 66 deletions

View File

@ -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);
});
});

View File

@ -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();
});

View File

@ -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<number>) => void;
reset: (value?: HookState<number>) => void;
}
export default function useCounter(
initialValue: number = 0,
initialValue: InitialHookState<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);
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<number>(initialValue);
const [get, setInternal] = useGetSet(init);
function set(value: number): void {
const current = get();
return [
get(),
useMemo(() => {
const set = (newState: HookState<number>) => {
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<number> = 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<number> = 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<number> = 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]),
];
}

View File

@ -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 = <T>(initialValue: T): [() => T, (value: T) => void] => {
const state = useRef(initialValue);
export default function useGetSet<S>(initialState: InitialHookState<S>): [() => S, Dispatch<HookState<S>>] {
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<S>) => {
state.current = resolveHookState(newState, state.current);
update();
},
],
[]
);
}

View File

@ -0,0 +1,16 @@
export type StateSetter<S> = (prevState: S) => S;
export type InitialStateSetter<S> = () => S;
export type InitialHookState<S> = S | InitialStateSetter<S>;
export type HookState<S> = S | StateSetter<S>;
export type ResolvableHookState<S> = S | StateSetter<S> | InitialStateSetter<S>;
export function resolveHookState<S, C extends S>(newState: StateSetter<S>, currentState: C): S;
export function resolveHookState<S, C extends S>(newState: ResolvableHookState<S>, currentState?: C): S;
export function resolveHookState<S, C extends S>(newState: ResolvableHookState<S>, currentState?: C): S {
if (typeof newState === 'function') {
return (newState as Function)(currentState);
}
return newState;
}