Skip to content

Commit

Permalink
Merge pull request #773 from streamich/fet-improve-useStateValidity
Browse files Browse the repository at this point in the history
feat: Improve useStateValidator & useMultiStateValidator typings
  • Loading branch information
streamich committed Nov 14, 2019
2 parents 6a00d2d + 436c210 commit 0073acb
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 53 deletions.
2 changes: 1 addition & 1 deletion src/__stories__/useMultiStateValidator.story.tsx
Expand Up @@ -41,7 +41,7 @@ const Demo = () => {
setState3((ev.target.value as unknown) as number);
}}
/>
{isValid !== null && <span style={{ marginLeft: 24 }}>{isValid ? 'Valid!' : 'Invalid'}</span>}
{isValid !== undefined && <span style={{ marginLeft: 24 }}>{isValid ? 'Valid!' : 'Invalid'}</span>}
</div>
);
};
Expand Down
8 changes: 4 additions & 4 deletions src/__stories__/useStateValidator.story.tsx
Expand Up @@ -3,9 +3,9 @@ import * as React from 'react';
import useStateValidator from '../useStateValidator';
import ShowDocs from './util/ShowDocs';

const DemoStateValidator = s => [s === '' ? null : (s * 1) % 2 === 0];
const DemoStateValidator = s => [s === '' ? undefined : (s * 1) % 2 === 0] as [boolean | undefined];
const Demo = () => {
const [state, setState] = React.useState<string | number>(0);
const [state, setState] = React.useState<number>(0);
const [[isValid]] = useStateValidator(state, DemoStateValidator);

return (
Expand All @@ -17,10 +17,10 @@ const Demo = () => {
max="10"
value={state}
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
setState(ev.target.value);
setState((ev.target.value as unknown) as number);
}}
/>
{isValid !== null && <span>{isValid ? 'Valid!' : 'Invalid'}</span>}
{isValid !== undefined && <span>{isValid ? 'Valid!' : 'Invalid'}</span>}
</div>
);
};
Expand Down
40 changes: 18 additions & 22 deletions src/useMultiStateValidator.ts
@@ -1,41 +1,37 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { DispatchValidity, UseValidatorReturn, ValidityState } from './useStateValidator';
import { StateValidator, UseStateValidatorReturn, ValidityState } from './useStateValidator';

export type MultiStateValidatorStates = any[] | { [p: string]: any } | { [p: number]: any };
export type MultiStateValidator<V extends ValidityState, S extends MultiStateValidatorStates> = StateValidator<V, S>;

export interface MultiStateValidator<
V extends ValidityState = ValidityState,
S extends MultiStateValidatorStates = MultiStateValidatorStates
> {
(states: S): V;

(states: S, done: DispatchValidity<V>): void;
}

export function useMultiStateValidator<
V extends ValidityState = ValidityState,
S extends MultiStateValidatorStates = MultiStateValidatorStates
>(states: S, validator: MultiStateValidator<V, S>, initialValidity: V = [undefined] as V): UseValidatorReturn<V> {
export function useMultiStateValidator<V extends ValidityState, S extends MultiStateValidatorStates, I extends V>(
states: S,
validator: MultiStateValidator<V, S>,
initialValidity: I = [undefined] as I
): UseStateValidatorReturn<V> {
if (typeof states !== 'object') {
throw new Error('states expected to be an object or array, got ' + typeof states);
}

const validatorFn = useRef(validator);
const validatorInner = useRef(validator);
const statesInner = useRef(states);

validatorInner.current = validator;
statesInner.current = states;

const [validity, setValidity] = useState(initialValidity);
const [validity, setValidity] = useState(initialValidity as V);

const deps = Array.isArray(states) ? states : Object.values(states);
const validate = useCallback(() => {
if (validatorFn.current.length === 2) {
validatorFn.current(states, setValidity);
if (validatorInner.current.length >= 2) {
validatorInner.current(statesInner.current, setValidity);
} else {
setValidity(validatorFn.current(states));
setValidity(validatorInner.current(statesInner.current));
}
}, deps);
}, [setValidity]);

useEffect(() => {
validate();
}, deps);
}, Object.values(states));

return [validity, validate];
}
39 changes: 21 additions & 18 deletions src/useStateValidator.ts
@@ -1,32 +1,35 @@
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 interface StateValidator<V, S> {
(state: S): V;

(state: S, dispatch: Dispatch<SetStateAction<V>>): void;
}

export type UseValidatorReturn<V extends ValidityState> = [V, () => void];
export type UseStateValidatorReturn<V> = [V, () => void];

export default function useStateValidator<V extends ValidityState, S = any>(
export default function useStateValidator<V extends ValidityState, S, I extends V>(
state: S,
validator: Validator<V, S>,
initialValidity: V = [undefined] as V
): UseValidatorReturn<V> {
const validatorFn = useRef(validator);
validator: StateValidator<V, S>,
initialState: I = [undefined] as I
): UseStateValidatorReturn<V> {
const validatorInner = useRef(validator);
const stateInner = useRef(state);

validatorInner.current = validator;
stateInner.current = state;

const [validity, setValidity] = useState(initialState as V);

const [validity, setValidity] = useState(initialValidity);
const validate = useCallback(() => {
if (validatorFn.current.length === 2) {
validatorFn.current(state, setValidity);
if (validatorInner.current.length >= 2) {
validatorInner.current(stateInner.current, setValidity as Dispatch<SetStateAction<V>>);
} else {
setValidity(validatorFn.current(state));
setValidity(validatorInner.current(stateInner.current));
}
}, [state]);
}, [setValidity]);

useEffect(() => {
validate();
Expand Down
6 changes: 3 additions & 3 deletions tests/useMultiStateValidator.test.ts
@@ -1,7 +1,7 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { useState } from 'react';
import { MultiStateValidator, useMultiStateValidator } from '../src/useMultiStateValidator';
import { UseValidatorReturn, ValidityState } from '../src/useStateValidator';
import { UseStateValidatorReturn, ValidityState } from '../src/useStateValidator';

interface Mock extends jest.Mock {}

Expand All @@ -16,7 +16,7 @@ describe('useMultiStateValidator', () => {
fn: MultiStateValidator<any, number[]> = jest.fn(defaultStatesValidator),
initialStates = [1, 2],
initialValidity = [false]
): [MultiStateValidator<any, number[]>, RenderHookResult<any, [Function, UseValidatorReturn<ValidityState>]>] {
): [MultiStateValidator<any, number[]>, RenderHookResult<any, [Function, UseStateValidatorReturn<ValidityState>]>] {
return [
fn,
renderHook(
Expand Down Expand Up @@ -114,7 +114,7 @@ describe('useMultiStateValidator', () => {
it('if validator expects 2nd parameters it should pass a validity setter there', () => {
const spy = (jest.fn((states: number[], done) => {
done([states.every(num => !!(num % 2))]);
}) as unknown) as MultiStateValidator;
}) as unknown) as MultiStateValidator<[boolean], number[]>;
const [, hook] = getHook(spy, [1, 3]);
const [, [validity]] = hook.result.current;

Expand Down
14 changes: 9 additions & 5 deletions tests/useStateValidator.test.ts
@@ -1,6 +1,6 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { useState } from 'react';
import useStateValidator, { UseValidatorReturn, Validator } from '../src/useStateValidator';
import useStateValidator, { StateValidator, UseStateValidatorReturn } from '../src/useStateValidator';

interface Mock extends jest.Mock {}

Expand All @@ -10,8 +10,8 @@ describe('useStateValidator', () => {
});

function getHook(
fn: Validator<any> = jest.fn(state => [!!(state % 2)])
): [jest.Mock | Function, RenderHookResult<any, [Function, UseValidatorReturn<any>]>] {
fn: StateValidator<[boolean], number> = jest.fn((state): [boolean] => [!!(state % 2)])
): [jest.Mock | Function, RenderHookResult<any, [Function, UseStateValidatorReturn<any>]>] {
return [
fn,
renderHook(() => {
Expand Down Expand Up @@ -80,8 +80,12 @@ describe('useStateValidator', () => {
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])));
it('if validator expects 2nd parameter it should pass a validity setter there', () => {
const [spy, hook] = getHook(
(jest.fn((state, setValidity): void => {
setValidity([state % 2 === 0]);
}) as unknown) as StateValidator<[boolean], number>
);
let [setState, [[isValid]]] = hook.result.current;

expect((spy as Mock).mock.calls[0].length).toBe(2);
Expand Down

0 comments on commit 0073acb

Please sign in to comment.