useValidatableState -> useStateValidator;

It is more suitable due to more flexible usage;
This commit is contained in:
xobotyi 2019-10-11 03:59:06 +03:00
parent 9543e23ca3
commit b998f3d397
7 changed files with 157 additions and 194 deletions

View File

@ -1,17 +1,17 @@
# `useValidatableState`
# `useStateValidator`
Very similar to React's `useState` hook, but extended with validation functionality.
Each time state changes validator invoked and it's result stored to separate state.
Each time given state changes - validator function is invoked.
## Usage
```ts
import * as React from 'react';
import { useCallback } from 'react';
import { useValidatableState } from 'react-use';
import { useStateValidator } from 'react-use';
const DemoStateValidator = s => [s === '' ? null : (s * 1) % 2 === 0];
const Demo = () => {
const validator = useCallback(s => [s === '' ? null : (s * 1) % 2 === 0], []);
const [state, setState, [isValid]] = useValidatableState<string>(validator, '');
const [state, setState] = React.useState<string | number>(0);
const [[isValid]] = useStateValidator(state, DemoStateValidator);
return (
<div>
@ -21,7 +21,7 @@ const Demo = () => {
min="0"
max="10"
value={state}
onChange={ev => {
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
setState(ev.target.value);
}}
/>
@ -33,16 +33,15 @@ const Demo = () => {
## Reference
```ts
const [state, setState, validity, revalidate] = useValidatableState(
validator: (state, prev, setValidity?)=>[boolean|null, ...any[]],
initialState: any
const [validity, revalidate] = useStateValidator(
state: any,
validator: (state, setValidity?)=>[boolean|null, ...any[]],
initialValidity: any
);
```
- `state` and `setState` are the same with React's `useState` hook;
- **`validity`**_`: [boolean|null, ...any[]]`_ result of validity check. First element is strictly nullable boolean, but others can contain arbitrary data;
- **`revalidate`**_`: ()=>void`_ runs validator once again
- **`validator`** should return an array suitable for validity state described above;
- **`validator`**_`: (state, setValidity?)=>[boolean|null, ...any[]]`_ should return an array suitable for validity state described above;
- `state` - current state;
- `prev` - previous state;
- `setValidity` - if defined hook will not trigger validity change automatically. Useful for async validators;
- `initialState` same with `useState` hook;
- `initialValidity` - validity value which set when validity is nt calculated yet;

View File

@ -1,12 +1,12 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useCallback } from 'react';
import { useValidatableState } from '../index';
import useStateValidator from '../useStateValidator';
import ShowDocs from './util/ShowDocs';
const DemoStateValidator = s => [s === '' ? null : (s * 1) % 2 === 0];
const Demo = () => {
const validator = useCallback(s => [s === '' ? null : (s * 1) % 2 === 0], []);
const [state, setState, [isValid]] = useValidatableState<string>(validator, '');
const [state, setState] = React.useState<string | number>(0);
const [[isValid]] = useStateValidator(state, DemoStateValidator);
return (
<div>
@ -16,7 +16,7 @@ const Demo = () => {
min="0"
max="10"
value={state}
onChange={ev => {
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
setState(ev.target.value);
}}
/>
@ -25,6 +25,6 @@ const Demo = () => {
);
};
storiesOf('State|useValidatableState', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useValidatableState.md')} />)
storiesOf('State|useStateValidator', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useStateValidator.md')} />)
.add('Demo', () => <Demo />);

View File

@ -0,0 +1,100 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { useState } from 'react';
import useStateValidator, { UseValidatorReturn, Validator } from '../useStateValidator';
interface Mock extends jest.Mock {}
describe('useStateValidator', () => {
it('should be defined', () => {
expect(useStateValidator).toBeDefined();
});
function getHook(
fn: Validator<any> = jest.fn(state => [!!(state % 2)])
): [jest.Mock | Function, RenderHookResult<any, [Function, UseValidatorReturn<any>]>] {
return [
fn,
renderHook(() => {
const [state, setState] = useState(1);
return [setState, useStateValidator(state, fn)];
}),
];
}
it('should return an array of two elements', () => {
const [, hook] = getHook();
const res = hook.result.current[1];
expect(Array.isArray(res)).toBe(true);
expect(res[0]).toEqual([true]);
expect(typeof res[1]).toBe('function');
});
it('first element should represent current validity state', () => {
const [, hook] = getHook();
let [setState, [validity]] = hook.result.current;
expect(validity).toEqual([true]);
act(() => setState(3));
[setState, [validity]] = hook.result.current;
expect(validity).toEqual([true]);
act(() => setState(4));
[setState, [validity]] = hook.result.current;
expect(validity).toEqual([false]);
});
it('second element should re-call validation', () => {
const [spy, hook] = getHook();
const [, [, revalidate]] = hook.result.current;
expect(spy).toHaveBeenCalledTimes(1);
act(() => revalidate());
expect(spy).toHaveBeenCalledTimes(2);
});
it('validator have to be called on init plus on each state update', () => {
const [spy, hook] = getHook(jest.fn());
const [setState] = hook.result.current;
expect(spy).toHaveBeenCalledTimes(1);
act(() => setState(4));
expect(spy).toHaveBeenCalledTimes(2);
act(() => setState(prevState => prevState + 1));
expect(spy).toHaveBeenCalledTimes(3);
});
it('should pass to validator one parameter - current state', () => {
const [spy, hook] = getHook(jest.fn());
const [setState] = hook.result.current;
act(() => setState(4));
act(() => setState(5));
expect((spy as Mock).mock.calls[0].length).toBe(1);
expect((spy as Mock).mock.calls[0].length).toBe(1);
expect((spy as Mock).mock.calls[0][0]).toBe(1);
expect((spy as Mock).mock.calls[1].length).toBe(1);
expect((spy as Mock).mock.calls[1][0]).toBe(4);
expect((spy as Mock).mock.calls[2].length).toBe(1);
expect((spy as Mock).mock.calls[2][0]).toBe(5);
});
it('if validator expects 2nd parameters it should pass a validity setter there', () => {
const [spy, hook] = getHook(jest.fn((state, setValidity) => setValidity!([state % 2 === 0])));
let [setState, [[isValid]]] = hook.result.current;
expect((spy as Mock).mock.calls[0].length).toBe(2);
expect(typeof (spy as Mock).mock.calls[0][1]).toBe('function');
expect(isValid).toBe(false);
act(() => setState(prevState => prevState + 1));
[setState, [[isValid]]] = hook.result.current;
expect(isValid).toBe(true);
act(() => setState(5));
[setState, [[isValid]]] = hook.result.current;
expect(isValid).toBe(false);
});
});

View File

@ -1,126 +0,0 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { useValidatableState } from '../index';
import { UseValidatableStateReturn, Validator } from '../useValidatableState';
interface Mock extends jest.Mock {}
describe('useValidatableState', () => {
it('should be defined', () => {
expect(useValidatableState).toBeDefined();
});
function getHook(
fn: Validator = jest.fn(() => {}),
initialState: any = null
): [Mock | Function, RenderHookResult<{ validator: Validator; init: any }, UseValidatableStateReturn>] {
return [
fn,
renderHook(({ validator, init }) => useValidatableState(validator as Function, init), {
initialProps: {
validator: fn,
init: initialState,
},
}),
];
}
it('should return an array of four elements', () => {
const [, hook] = getHook();
expect(Array.isArray(hook.result.current)).toBe(true);
expect(hook.result.current.length).toBe(4);
});
it('first two elements should act like regular setState', () => {
const [, hook] = getHook(jest.fn(), 3);
const [, setState] = hook.result.current;
expect(hook.result.current[0]).toBe(3);
act(() => setState(4));
expect(hook.result.current[0]).toBe(4);
act(() => setState(prevState => prevState + 1));
expect(hook.result.current[0]).toBe(5);
});
it('validator have to be called on init plus on each state update', () => {
const [spy, hook] = getHook(jest.fn(), 3);
const [, setState] = hook.result.current;
expect(spy).toHaveBeenCalledTimes(1);
act(() => setState(4));
expect(spy).toHaveBeenCalledTimes(2);
act(() => setState(prevState => prevState + 1));
expect(spy).toHaveBeenCalledTimes(3);
});
it('third element of returned array should represent validity state', () => {
const [, hook] = getHook(jest.fn(state => [state % 2 === 0]), 3);
let [, setState, [isValid]] = hook.result.current;
expect(isValid).toBe(false);
act(() => setState(prevState => prevState + 1));
[, setState, [isValid]] = hook.result.current;
expect(isValid).toBe(true);
act(() => setState(5));
[, setState, [isValid]] = hook.result.current;
expect(isValid).toBe(false);
});
it('should recalculate validity on validator change', () => {
const [, hook] = getHook(jest.fn(state => [state % 2 === 0]), 3);
let [, setState, [isValid]] = hook.result.current;
expect(isValid).toBe(false);
hook.rerender({ validator: jest.fn(state => [state % 2 === 1]), init: 3 });
[, setState, [isValid]] = hook.result.current;
expect(isValid).toBe(true);
act(() => setState(prevState => prevState + 1));
[, setState, [isValid]] = hook.result.current;
expect(isValid).toBe(false);
});
it('forth element of returned array should re-call validation', () => {
const [spy, hook] = getHook(jest.fn(), 3);
const [, , , validate] = hook.result.current;
expect(spy).toHaveBeenCalledTimes(1);
act(() => validate());
expect(spy).toHaveBeenCalledTimes(2);
});
it('should pass to validator two parameters: first - current state, second - previous state', () => {
const [spy, hook] = getHook(jest.fn(), 3);
const [, setState] = hook.result.current;
act(() => setState(4));
act(() => setState(prevState => prevState + 1));
expect((spy as Mock).mock.calls[0][0]).toBe(3);
expect((spy as Mock).mock.calls[0][1]).toBe(null);
expect((spy as Mock).mock.calls[1][0]).toBe(4);
expect((spy as Mock).mock.calls[1][1]).toBe(3);
expect((spy as Mock).mock.calls[2][0]).toBe(5);
expect((spy as Mock).mock.calls[2][1]).toBe(4);
});
it('if validator expects 3 parameters it should pass a validity setter there', () => {
const [spy, hook] = getHook(jest.fn((state, _prevState, setValidity) => setValidity!([state % 2 === 0])), 3);
let [, setState, [isValid]] = hook.result.current;
expect(typeof (spy as Mock).mock.calls[0][2]).toBe('function');
expect(isValid).toBe(false);
act(() => setState(prevState => prevState + 1));
[, setState, [isValid]] = hook.result.current;
expect(isValid).toBe(true);
act(() => setState(5));
[, setState, [isValid]] = hook.result.current;
expect(isValid).toBe(false);
});
});

View File

@ -86,7 +86,7 @@ export { default as useUpdate } from './useUpdate';
export { default as useUpdateEffect } from './useUpdateEffect';
export { default as useUpsert } from './useUpsert';
export { default as useVideo } from './useVideo';
export { default as useValidatableState } from './useValidatableState';
export { default as useStateValidator } from './useStateValidator';
export { useWait, Waiter } from './useWait';
export { default as useWindowScroll } from './useWindowScroll';
export { default as useWindowSize } from './useWindowSize';

36
src/useStateValidator.ts Normal file
View File

@ -0,0 +1,36 @@
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
export type ValidityState = [boolean | undefined, ...any[]];
export type DispatchValidity<V extends ValidityState> = Dispatch<SetStateAction<V>>;
export type Validator<V extends ValidityState, S = any> =
| {
(state?: S): V;
(state?: S, dispatch?: DispatchValidity<V>): void;
}
| Function;
export type UseValidatorReturn<V extends ValidityState> = [V, () => void];
export default function useStateValidator<V extends ValidityState, S = any>(
state: S,
validator: Validator<V, S>,
initialValidity: V = [undefined] as V
): UseValidatorReturn<V> {
const validatorFn = useRef(validator);
const [validity, setValidity] = useState(initialValidity);
const validate = useCallback(() => {
if (validatorFn.current.length === 2) {
validatorFn.current(state, setValidity);
} else {
setValidity(validatorFn.current(state));
}
}, [state]);
useEffect(() => {
validate();
}, [state]);
return [validity, validate];
}

View File

@ -1,46 +0,0 @@
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
export type ValidityState = [boolean | null, ...any[]];
export type DispatchValidityState = Dispatch<SetStateAction<ValidityState>>;
export type Validator<State = any, StateValidity extends ValidityState = ValidityState> =
| {
(state?: State, prev?: State): StateValidity;
(state?: State, prev?: State, setValidity?: DispatchValidityState): void;
}
| Function;
export type ValidateFn = () => void;
export type UseValidatableStateReturn<State = any, StateValidity extends ValidityState = ValidityState> = [
State,
Dispatch<SetStateAction<State>>,
StateValidity,
ValidateFn
];
export default function useValidatableState<State, StateValidity extends ValidityState = ValidityState>(
validator: Validator<State | null, StateValidity>,
initialState?: State
): UseValidatableStateReturn<State, StateValidity> {
const prevState = useRef<State | null>(null);
const [state, setState] = useState<State>(initialState!);
const [validity, setValidity] = useState([null] as StateValidity);
const validate = useCallback<ValidateFn>(() => {
if (validator.length === 3) {
validator(state, prevState.current, setValidity as DispatchValidityState);
} else {
setValidity(validator(state, prevState.current));
}
}, [state, validator]);
useEffect(() => {
validate();
}, [validate, state]);
useEffect(() => {
prevState.current = state;
}, [state]);
return [state, setState, validity, validate];
}