From 9543e23ca3c9293959f3ab94c1b78ddea1f279e2 Mon Sep 17 00:00:00 2001 From: xobotyi Date: Wed, 4 Sep 2019 04:10:11 +0300 Subject: [PATCH 1/3] useValidatableState implementation; --- README.md | 1 + docs/useValidatableState.md | 48 ++++++++ src/__stories__/useValidityState.story.tsx | 30 +++++ src/__tests__/useValidatableState.test.ts | 126 +++++++++++++++++++++ src/index.ts | 1 + src/useValidatableState.ts | 46 ++++++++ 6 files changed, 252 insertions(+) create mode 100644 docs/useValidatableState.md create mode 100644 src/__stories__/useValidityState.story.tsx create mode 100644 src/__tests__/useValidatableState.test.ts create mode 100644 src/useValidatableState.ts diff --git a/README.md b/README.md index f54afa21d6..c08670cc64 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ - [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usecounter--demo) - [`useList`](./docs/useList.md) — tracks state of an array. - [`useMap`](./docs/useMap.md) — tracks state of an object. + - [`useValidatableState`](./docs/useValidatableState.md) — tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usevalidatablestate--demo)
diff --git a/docs/useValidatableState.md b/docs/useValidatableState.md new file mode 100644 index 0000000000..1c84449eea --- /dev/null +++ b/docs/useValidatableState.md @@ -0,0 +1,48 @@ +# `useValidatableState` + +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. + +## Usage +```ts +import * as React from 'react'; +import { useCallback } from 'react'; +import { useValidatableState } from 'react-use'; + +const Demo = () => { + const validator = useCallback(s => [s === '' ? null : (s * 1) % 2 === 0], []); + const [state, setState, [isValid]] = useValidatableState(validator, ''); + + return ( +
+
Below field is valid only if number is even
+ { + setState(ev.target.value); + }} + /> + {isValid !== null && {isValid ? 'Valid!' : 'Invalid'}} +
+ ); +}; +``` + +## Reference +```ts +const [state, setState, validity, revalidate] = useValidatableState( + validator: (state, prev, setValidity?)=>[boolean|null, ...any[]], + initialState: 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; + - `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; diff --git a/src/__stories__/useValidityState.story.tsx b/src/__stories__/useValidityState.story.tsx new file mode 100644 index 0000000000..66e9651e17 --- /dev/null +++ b/src/__stories__/useValidityState.story.tsx @@ -0,0 +1,30 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { useCallback } from 'react'; +import { useValidatableState } from '../index'; +import ShowDocs from './util/ShowDocs'; + +const Demo = () => { + const validator = useCallback(s => [s === '' ? null : (s * 1) % 2 === 0], []); + const [state, setState, [isValid]] = useValidatableState(validator, ''); + + return ( +
+
Below field is valid only if number is even
+ { + setState(ev.target.value); + }} + /> + {isValid !== null && {isValid ? 'Valid!' : 'Invalid'}} +
+ ); +}; + +storiesOf('State|useValidatableState', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/src/__tests__/useValidatableState.test.ts b/src/__tests__/useValidatableState.test.ts new file mode 100644 index 0000000000..0e0bc515c3 --- /dev/null +++ b/src/__tests__/useValidatableState.test.ts @@ -0,0 +1,126 @@ +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); + }); +}); diff --git a/src/index.ts b/src/index.ts index b3514a7179..412e1bd707 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,6 +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 { useWait, Waiter } from './useWait'; export { default as useWindowScroll } from './useWindowScroll'; export { default as useWindowSize } from './useWindowSize'; diff --git a/src/useValidatableState.ts b/src/useValidatableState.ts new file mode 100644 index 0000000000..2d0b9b4647 --- /dev/null +++ b/src/useValidatableState.ts @@ -0,0 +1,46 @@ +import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; + +export type ValidityState = [boolean | null, ...any[]]; +export type DispatchValidityState = Dispatch>; + +export type Validator = + | { + (state?: State, prev?: State): StateValidity; + (state?: State, prev?: State, setValidity?: DispatchValidityState): void; + } + | Function; + +export type ValidateFn = () => void; + +export type UseValidatableStateReturn = [ + State, + Dispatch>, + StateValidity, + ValidateFn +]; + +export default function useValidatableState( + validator: Validator, + initialState?: State +): UseValidatableStateReturn { + const prevState = useRef(null); + const [state, setState] = useState(initialState!); + const [validity, setValidity] = useState([null] as StateValidity); + + const validate = useCallback(() => { + 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]; +} From b998f3d3974b7ea0d114f7bdd1c26a75a40ac09e Mon Sep 17 00:00:00 2001 From: xobotyi Date: Fri, 11 Oct 2019 03:59:06 +0300 Subject: [PATCH 2/3] useValidatableState -> useStateValidator; It is more suitable due to more flexible usage; --- ...lidatableState.md => useStateValidator.md} | 27 ++-- ...yState.story.tsx => useStateValidator.tsx} | 14 +- src/__tests__/useStateValidator.test.ts | 100 ++++++++++++++ src/__tests__/useValidatableState.test.ts | 126 ------------------ src/index.ts | 2 +- src/useStateValidator.ts | 36 +++++ src/useValidatableState.ts | 46 ------- 7 files changed, 157 insertions(+), 194 deletions(-) rename docs/{useValidatableState.md => useStateValidator.md} (50%) rename src/__stories__/{useValidityState.story.tsx => useStateValidator.tsx} (52%) create mode 100644 src/__tests__/useStateValidator.test.ts delete mode 100644 src/__tests__/useValidatableState.test.ts create mode 100644 src/useStateValidator.ts delete mode 100644 src/useValidatableState.ts diff --git a/docs/useValidatableState.md b/docs/useStateValidator.md similarity index 50% rename from docs/useValidatableState.md rename to docs/useStateValidator.md index 1c84449eea..0b434fcaf9 100644 --- a/docs/useValidatableState.md +++ b/docs/useStateValidator.md @@ -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(validator, ''); + const [state, setState] = React.useState(0); + const [[isValid]] = useStateValidator(state, DemoStateValidator); return (
@@ -21,7 +21,7 @@ const Demo = () => { min="0" max="10" value={state} - onChange={ev => { + onChange={(ev: React.ChangeEvent) => { 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; diff --git a/src/__stories__/useValidityState.story.tsx b/src/__stories__/useStateValidator.tsx similarity index 52% rename from src/__stories__/useValidityState.story.tsx rename to src/__stories__/useStateValidator.tsx index 66e9651e17..906de0cbf6 100644 --- a/src/__stories__/useValidityState.story.tsx +++ b/src/__stories__/useStateValidator.tsx @@ -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(validator, ''); + const [state, setState] = React.useState(0); + const [[isValid]] = useStateValidator(state, DemoStateValidator); return (
@@ -16,7 +16,7 @@ const Demo = () => { min="0" max="10" value={state} - onChange={ev => { + onChange={(ev: React.ChangeEvent) => { setState(ev.target.value); }} /> @@ -25,6 +25,6 @@ const Demo = () => { ); }; -storiesOf('State|useValidatableState', module) - .add('Docs', () => ) +storiesOf('State|useStateValidator', module) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/src/__tests__/useStateValidator.test.ts b/src/__tests__/useStateValidator.test.ts new file mode 100644 index 0000000000..7b71754137 --- /dev/null +++ b/src/__tests__/useStateValidator.test.ts @@ -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 = jest.fn(state => [!!(state % 2)]) + ): [jest.Mock | Function, RenderHookResult]>] { + 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); + }); +}); diff --git a/src/__tests__/useValidatableState.test.ts b/src/__tests__/useValidatableState.test.ts deleted file mode 100644 index 0e0bc515c3..0000000000 --- a/src/__tests__/useValidatableState.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/index.ts b/src/index.ts index 412e1bd707..a3a933feb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/useStateValidator.ts b/src/useStateValidator.ts new file mode 100644 index 0000000000..cf9070f0a3 --- /dev/null +++ b/src/useStateValidator.ts @@ -0,0 +1,36 @@ +import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; + +export type ValidityState = [boolean | undefined, ...any[]]; +export type DispatchValidity = Dispatch>; + +export type Validator = + | { + (state?: S): V; + (state?: S, dispatch?: DispatchValidity): void; + } + | Function; + +export type UseValidatorReturn = [V, () => void]; + +export default function useStateValidator( + state: S, + validator: Validator, + initialValidity: V = [undefined] as V +): UseValidatorReturn { + 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]; +} diff --git a/src/useValidatableState.ts b/src/useValidatableState.ts deleted file mode 100644 index 2d0b9b4647..0000000000 --- a/src/useValidatableState.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; - -export type ValidityState = [boolean | null, ...any[]]; -export type DispatchValidityState = Dispatch>; - -export type Validator = - | { - (state?: State, prev?: State): StateValidity; - (state?: State, prev?: State, setValidity?: DispatchValidityState): void; - } - | Function; - -export type ValidateFn = () => void; - -export type UseValidatableStateReturn = [ - State, - Dispatch>, - StateValidity, - ValidateFn -]; - -export default function useValidatableState( - validator: Validator, - initialState?: State -): UseValidatableStateReturn { - const prevState = useRef(null); - const [state, setState] = useState(initialState!); - const [validity, setValidity] = useState([null] as StateValidity); - - const validate = useCallback(() => { - 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]; -} From 1920f8b135cd64ce2fb9a534cdb4d85e779a060b Mon Sep 17 00:00:00 2001 From: xobotyi Date: Fri, 11 Oct 2019 04:26:45 +0300 Subject: [PATCH 3/3] Readme update --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c08670cc64..4c554d308d 100644 --- a/README.md +++ b/README.md @@ -131,9 +131,9 @@ - [`useStateList`](./docs/useStateList.md) — circularly iterates over an array. - [`useToggle` and `useBoolean`](./docs/useToggle.md) — tracks state of a boolean. - [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usecounter--demo) - - [`useList`](./docs/useList.md) — tracks state of an array. - - [`useMap`](./docs/useMap.md) — tracks state of an object. - - [`useValidatableState`](./docs/useValidatableState.md) — tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usevalidatablestate--demo) + - [`useList`](./docs/useList.md) and [`useUpsert`](./docs/useUpsert.md) — tracks state of an array. [![][img-demo]](https://codesandbox.io/s/wonderful-mahavira-1sm0w) + - [`useMap`](./docs/useMap.md) — tracks state of an object. [![][img-demo]](https://codesandbox.io/s/quirky-dewdney-gi161) + - [`useStateValidator`](./docs/useStateValidator.md) — tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo)