-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #588 from streamich/feat-useValidatableState
feat: useStateValidator
- Loading branch information
Showing
6 changed files
with
215 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# `useStateValidator` | ||
|
||
Each time given state changes - validator function is invoked. | ||
|
||
## Usage | ||
```ts | ||
import * as React from 'react'; | ||
import { useCallback } from 'react'; | ||
import { useStateValidator } from 'react-use'; | ||
|
||
const DemoStateValidator = s => [s === '' ? null : (s * 1) % 2 === 0]; | ||
const Demo = () => { | ||
const [state, setState] = React.useState<string | number>(0); | ||
const [[isValid]] = useStateValidator(state, DemoStateValidator); | ||
|
||
return ( | ||
<div> | ||
<div>Below field is valid only if number is even</div> | ||
<input | ||
type="number" | ||
min="0" | ||
max="10" | ||
value={state} | ||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => { | ||
setState(ev.target.value); | ||
}} | ||
/> | ||
{isValid !== null && <span>{isValid ? 'Valid!' : 'Invalid'}</span>} | ||
</div> | ||
); | ||
}; | ||
``` | ||
|
||
## Reference | ||
```ts | ||
const [validity, revalidate] = useStateValidator( | ||
state: any, | ||
validator: (state, setValidity?)=>[boolean|null, ...any[]], | ||
initialValidity: any | ||
); | ||
``` | ||
- **`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`**_`: (state, setValidity?)=>[boolean|null, ...any[]]`_ should return an array suitable for validity state described above; | ||
- `state` - current state; | ||
- `setValidity` - if defined hook will not trigger validity change automatically. Useful for async validators; | ||
- `initialValidity` - validity value which set when validity is nt calculated yet; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { storiesOf } from '@storybook/react'; | ||
import * as React from 'react'; | ||
import useStateValidator from '../useStateValidator'; | ||
import ShowDocs from './util/ShowDocs'; | ||
|
||
const DemoStateValidator = s => [s === '' ? null : (s * 1) % 2 === 0]; | ||
const Demo = () => { | ||
const [state, setState] = React.useState<string | number>(0); | ||
const [[isValid]] = useStateValidator(state, DemoStateValidator); | ||
|
||
return ( | ||
<div> | ||
<div>Below field is valid only if number is even</div> | ||
<input | ||
type="number" | ||
min="0" | ||
max="10" | ||
value={state} | ||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => { | ||
setState(ev.target.value); | ||
}} | ||
/> | ||
{isValid !== null && <span>{isValid ? 'Valid!' : 'Invalid'}</span>} | ||
</div> | ||
); | ||
}; | ||
|
||
storiesOf('State|useStateValidator', module) | ||
.add('Docs', () => <ShowDocs md={require('../../docs/useStateValidator.md')} />) | ||
.add('Demo', () => <Demo />); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]; | ||
} |