mirror of
https://github.com/streamich/react-use.git
synced 2026-01-25 14:17:16 +00:00
Merge pull request #723 from streamich/resolveHookState-implementation
feat: react-like state resolver
This commit is contained in:
commit
b8b49b3a60
26
src/__tests__/resolveHookState.ts
Normal file
26
src/__tests__/resolveHookState.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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]),
|
||||
];
|
||||
}
|
||||
|
||||
@ -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();
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
16
src/util/resolveHookState.ts
Normal file
16
src/util/resolveHookState.ts
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user