mirror of
https://github.com/streamich/react-use.git
synced 2025-12-08 18:02:14 +00:00
useValidatableState -> useStateValidator;
It is more suitable due to more flexible usage;
This commit is contained in:
parent
9543e23ca3
commit
b998f3d397
@ -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;
|
||||
@ -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 />);
|
||||
100
src/__tests__/useStateValidator.test.ts
Normal file
100
src/__tests__/useStateValidator.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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
36
src/useStateValidator.ts
Normal 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];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user