Skip to content

Commit 436c210

Browse files
committed
feat(useStateValidator): Refactor method and improve typings;
feat(useMultiStateValidator): Refactor method and improve typings;
1 parent b5b9e52 commit 436c210

6 files changed

+56
-53
lines changed

src/__stories__/useMultiStateValidator.story.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const Demo = () => {
4141
setState3((ev.target.value as unknown) as number);
4242
}}
4343
/>
44-
{isValid !== null && <span style={{ marginLeft: 24 }}>{isValid ? 'Valid!' : 'Invalid'}</span>}
44+
{isValid !== undefined && <span style={{ marginLeft: 24 }}>{isValid ? 'Valid!' : 'Invalid'}</span>}
4545
</div>
4646
);
4747
};

src/__stories__/useStateValidator.story.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import * as React from 'react';
33
import useStateValidator from '../useStateValidator';
44
import ShowDocs from './util/ShowDocs';
55

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

1111
return (
@@ -17,10 +17,10 @@ const Demo = () => {
1717
max="10"
1818
value={state}
1919
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
20-
setState(ev.target.value);
20+
setState((ev.target.value as unknown) as number);
2121
}}
2222
/>
23-
{isValid !== null && <span>{isValid ? 'Valid!' : 'Invalid'}</span>}
23+
{isValid !== undefined && <span>{isValid ? 'Valid!' : 'Invalid'}</span>}
2424
</div>
2525
);
2626
};

src/useMultiStateValidator.ts

+18-22
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,37 @@
11
import { useCallback, useEffect, useRef, useState } from 'react';
2-
import { DispatchValidity, UseValidatorReturn, ValidityState } from './useStateValidator';
2+
import { StateValidator, UseStateValidatorReturn, ValidityState } from './useStateValidator';
33

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

6-
export interface MultiStateValidator<
7-
V extends ValidityState = ValidityState,
8-
S extends MultiStateValidatorStates = MultiStateValidatorStates
9-
> {
10-
(states: S): V;
11-
12-
(states: S, done: DispatchValidity<V>): void;
13-
}
14-
15-
export function useMultiStateValidator<
16-
V extends ValidityState = ValidityState,
17-
S extends MultiStateValidatorStates = MultiStateValidatorStates
18-
>(states: S, validator: MultiStateValidator<V, S>, initialValidity: V = [undefined] as V): UseValidatorReturn<V> {
7+
export function useMultiStateValidator<V extends ValidityState, S extends MultiStateValidatorStates, I extends V>(
8+
states: S,
9+
validator: MultiStateValidator<V, S>,
10+
initialValidity: I = [undefined] as I
11+
): UseStateValidatorReturn<V> {
1912
if (typeof states !== 'object') {
2013
throw new Error('states expected to be an object or array, got ' + typeof states);
2114
}
2215

23-
const validatorFn = useRef(validator);
16+
const validatorInner = useRef(validator);
17+
const statesInner = useRef(states);
18+
19+
validatorInner.current = validator;
20+
statesInner.current = states;
2421

25-
const [validity, setValidity] = useState(initialValidity);
22+
const [validity, setValidity] = useState(initialValidity as V);
2623

27-
const deps = Array.isArray(states) ? states : Object.values(states);
2824
const validate = useCallback(() => {
29-
if (validatorFn.current.length === 2) {
30-
validatorFn.current(states, setValidity);
25+
if (validatorInner.current.length >= 2) {
26+
validatorInner.current(statesInner.current, setValidity);
3127
} else {
32-
setValidity(validatorFn.current(states));
28+
setValidity(validatorInner.current(statesInner.current));
3329
}
34-
}, deps);
30+
}, [setValidity]);
3531

3632
useEffect(() => {
3733
validate();
38-
}, deps);
34+
}, Object.values(states));
3935

4036
return [validity, validate];
4137
}

src/useStateValidator.ts

+21-18
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
11
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
22

33
export type ValidityState = [boolean | undefined, ...any[]];
4-
export type DispatchValidity<V extends ValidityState> = Dispatch<SetStateAction<V>>;
54

6-
export type Validator<V extends ValidityState, S = any> =
7-
| {
8-
(state?: S): V;
9-
(state?: S, dispatch?: DispatchValidity<V>): void;
10-
}
11-
| Function;
5+
export interface StateValidator<V, S> {
6+
(state: S): V;
7+
8+
(state: S, dispatch: Dispatch<SetStateAction<V>>): void;
9+
}
1210

13-
export type UseValidatorReturn<V extends ValidityState> = [V, () => void];
11+
export type UseStateValidatorReturn<V> = [V, () => void];
1412

15-
export default function useStateValidator<V extends ValidityState, S = any>(
13+
export default function useStateValidator<V extends ValidityState, S, I extends V>(
1614
state: S,
17-
validator: Validator<V, S>,
18-
initialValidity: V = [undefined] as V
19-
): UseValidatorReturn<V> {
20-
const validatorFn = useRef(validator);
15+
validator: StateValidator<V, S>,
16+
initialState: I = [undefined] as I
17+
): UseStateValidatorReturn<V> {
18+
const validatorInner = useRef(validator);
19+
const stateInner = useRef(state);
20+
21+
validatorInner.current = validator;
22+
stateInner.current = state;
23+
24+
const [validity, setValidity] = useState(initialState as V);
2125

22-
const [validity, setValidity] = useState(initialValidity);
2326
const validate = useCallback(() => {
24-
if (validatorFn.current.length === 2) {
25-
validatorFn.current(state, setValidity);
27+
if (validatorInner.current.length >= 2) {
28+
validatorInner.current(stateInner.current, setValidity as Dispatch<SetStateAction<V>>);
2629
} else {
27-
setValidity(validatorFn.current(state));
30+
setValidity(validatorInner.current(stateInner.current));
2831
}
29-
}, [state]);
32+
}, [setValidity]);
3033

3134
useEffect(() => {
3235
validate();

tests/useMultiStateValidator.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
22
import { useState } from 'react';
33
import { MultiStateValidator, useMultiStateValidator } from '../src/useMultiStateValidator';
4-
import { UseValidatorReturn, ValidityState } from '../src/useStateValidator';
4+
import { UseStateValidatorReturn, ValidityState } from '../src/useStateValidator';
55

66
interface Mock extends jest.Mock {}
77

@@ -16,7 +16,7 @@ describe('useMultiStateValidator', () => {
1616
fn: MultiStateValidator<any, number[]> = jest.fn(defaultStatesValidator),
1717
initialStates = [1, 2],
1818
initialValidity = [false]
19-
): [MultiStateValidator<any, number[]>, RenderHookResult<any, [Function, UseValidatorReturn<ValidityState>]>] {
19+
): [MultiStateValidator<any, number[]>, RenderHookResult<any, [Function, UseStateValidatorReturn<ValidityState>]>] {
2020
return [
2121
fn,
2222
renderHook(
@@ -114,7 +114,7 @@ describe('useMultiStateValidator', () => {
114114
it('if validator expects 2nd parameters it should pass a validity setter there', () => {
115115
const spy = (jest.fn((states: number[], done) => {
116116
done([states.every(num => !!(num % 2))]);
117-
}) as unknown) as MultiStateValidator;
117+
}) as unknown) as MultiStateValidator<[boolean], number[]>;
118118
const [, hook] = getHook(spy, [1, 3]);
119119
const [, [validity]] = hook.result.current;
120120

tests/useStateValidator.test.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
22
import { useState } from 'react';
3-
import useStateValidator, { UseValidatorReturn, Validator } from '../src/useStateValidator';
3+
import useStateValidator, { StateValidator, UseStateValidatorReturn } from '../src/useStateValidator';
44

55
interface Mock extends jest.Mock {}
66

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

1212
function getHook(
13-
fn: Validator<any> = jest.fn(state => [!!(state % 2)])
14-
): [jest.Mock | Function, RenderHookResult<any, [Function, UseValidatorReturn<any>]>] {
13+
fn: StateValidator<[boolean], number> = jest.fn((state): [boolean] => [!!(state % 2)])
14+
): [jest.Mock | Function, RenderHookResult<any, [Function, UseStateValidatorReturn<any>]>] {
1515
return [
1616
fn,
1717
renderHook(() => {
@@ -80,8 +80,12 @@ describe('useStateValidator', () => {
8080
expect((spy as Mock).mock.calls[2][0]).toBe(5);
8181
});
8282

83-
it('if validator expects 2nd parameters it should pass a validity setter there', () => {
84-
const [spy, hook] = getHook(jest.fn((state, setValidity) => setValidity!([state % 2 === 0])));
83+
it('if validator expects 2nd parameter it should pass a validity setter there', () => {
84+
const [spy, hook] = getHook(
85+
(jest.fn((state, setValidity): void => {
86+
setValidity([state % 2 === 0]);
87+
}) as unknown) as StateValidator<[boolean], number>
88+
);
8589
let [setState, [[isValid]]] = hook.result.current;
8690

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

0 commit comments

Comments
 (0)