Skip to content

Commit ba66305

Browse files
committed
[IMPL] useStateValidator hook
1 parent 934b1f4 commit ba66305

10 files changed

Lines changed: 182 additions & 9 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useStateValidator } from "../../../../../../packages/react-tools/src"
2+
3+
/**
4+
The component uses _useStateValidator_ hook to declare a state object with _name_ and _email_ properties attached to two input tag and validates them during typing.
5+
*/
6+
export const UseStateValidator = () => {
7+
const [state, setState, validation] = useStateValidator(
8+
{
9+
name: "", email: ""
10+
},
11+
(state, validation) => {
12+
if (state.name.length > 10) {
13+
validation.name.invalid = true;
14+
validation.name.message = "Max Length 10 characters"
15+
}
16+
if (!state.email.includes("@")) {
17+
validation.email.invalid = true;
18+
validation.email.message = "@ is missing"
19+
}
20+
return validation;
21+
}
22+
);
23+
24+
return <div>
25+
<div style={{display: "flex", flexDirection: "column", width: 'fit-content', margin: "0 auto"}}>
26+
<input type="text" name="name" value={state.name} onChange={e => setState(s => ({...s, [e.target.name]: e.target.value}))} />
27+
{
28+
validation.name.invalid &&
29+
<span style={{ color: "red" }}>{validation.name.message}</span>
30+
}
31+
</div>
32+
<div style={{display: "flex", flexDirection: "column", width: 'fit-content', margin: "0 auto"}}>
33+
<input type="text" name="email" value={state.email} onChange={e => setState(s => ({...s, [e.target.name]: e.target.value}))} />
34+
{
35+
validation.email.invalid &&
36+
<span style={{ color: "red" }}>{validation.email.message}</span>
37+
}
38+
</div>
39+
</div>
40+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export const COMPONENTS = [
1818
"useArray",
1919
"useProxyState",
2020
"useSyncExternalStore",
21-
"useDerivedState"
21+
"useDerivedState",
22+
"useStateValidator"
2223
],
2324
//LIFECYCLE
2425
[

apps/react-tools-demo/src/markdown/useBroadcastChannel.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ export const UseBroadcastChannel = () => {
1010
return <div>
1111
<h2>Open page on multiple tab to see how hook work</h2>
1212
<p>State: {state}</p>
13-
<form onSubmit={(e: FormEvent<HTMLFormElement>) => { debugger }}>
13+
<form
14+
onSubmit={(e: FormEvent<HTMLFormElement>) => {
15+
e.preventDefault();
16+
setState(((e.target as HTMLFormElement).elements.namedItem("text") as HTMLInputElement).value)
17+
}}
18+
>
1419
<input name="text" type="text" />
1520
<button type="submit">SEND</button>
1621
</form>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# useStateValidator
2+
Custom _useState_ hook that validates state on every update.
3+
4+
## Usage
5+
6+
```tsx
7+
export const UseStateValidator = () => {
8+
const [state, setState, validation] = useStateValidator(
9+
{
10+
name: "", email: ""
11+
},
12+
(state, validation) => {
13+
if (state.name.length > 10) {
14+
validation.name.result = false;
15+
validation.name.message = "Max Length 10 characters"
16+
}
17+
if (!state.email.includes("@")) {
18+
validation.email.result = false;
19+
validation.email.message = "@ is missing"
20+
}
21+
return validation;
22+
}
23+
);
24+
25+
return <div>
26+
<div>
27+
<input type="text" name="name" value={state.name} onChange={e => setState(s => ({...s, [e.target.name]: e.target.value}))} />
28+
{
29+
validation.name.result &&
30+
<span style={{ color: "red" }}>{validation.name.message}</span>
31+
}
32+
</div>
33+
<div>
34+
<input type="text" name="email" value={state.email} onChange={e => setState(s => ({...s, [e.target.name]: e.target.value}))} />
35+
{
36+
validation.email.result &&
37+
<span style={{ color: "red" }}>{validation.email.message}</span>
38+
}
39+
</div>
40+
</div>
41+
}
42+
```
43+
44+
> The component uses _useStateValidator_ hook to declare a state object with _name_ and _email_ properties attached to two input tag and validates them during typing.
45+
46+
47+
## API
48+
49+
```tsx
50+
useStateValidator<T>(initialState: T | (() => T), validator: StateValidator<T>): [T, Dispatch<SetStateAction<T>>, T extends Record<string, unknown> ? {[k in keyof T]:{result: boolean, message?: string}} : {result: boolean, message?: string}]
51+
```
52+
53+
> ### Params
54+
>
55+
> - __initialState__: _T | () => T_
56+
value or a function.
57+
> - __validator__: _StateValidator_
58+
function that will be executed to validate state.
59+
>
60+
61+
> ### Returns
62+
>
63+
> __} result__: __Array__:
64+
- _T_
65+
- _Dispatch<SetStateAction<T>>_
66+
- _T extends Record<string, unknown> ? {[k in keyof T]:{result: boolean, message?: string}} : {result: boolean, message?: strin_
67+
> Array with:
68+
> - first element: __state__ value.
69+
> - second element: __setState__ function to update state.
70+
> - third element: __valid__ validation value/object for state.
71+
>

packages/react-tools/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
- [x] useProxyState
1616
- [x] useSyncExternalStore
1717
- [x] useDerivedState
18-
- [ ] useStateValidator (???)
18+
- [x] useStateValidator
19+
- [ ] usePubSubStore (with pusSub model)
20+
- [ ] createPubSubStore (with pusSub model)
1921
- [ ] useObservable — (https://netbasal.com/javascript-observables-under-the-hood-2423f760584)
2022
- [ ] useSignal (https://medium.com/@personal.david.kohen/the-quest-for-signals-in-react-usestate-on-steroids-71eb9fc87c14)
2123
- [ ] createSignal (https://medium.com/@personal.david.kohen/the-quest-for-signals-in-react-usestate-on-steroids-71eb9fc87c14)
22-
- [ ] usePubSubStore (with pusSub model)
23-
- [ ] createPubSubStore (with pusSub model)
2424
- [ ] useProxyStore (TODO: https://github.com/streamich/react-use/blob/master/src/factory/createGlobalState.ts)
2525
- [ ] createProxyStore (TODO: https://github.com/streamich/react-use/blob/master/src/factory/createGlobalState.ts)
2626

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,5 @@ export { useWebWorkerFn } from './useWebWorkerFn';
102102
export { usePromiseSuspensible } from './usePromiseSuspensible';
103103
export { useFetch } from './useFetch';
104104
export { useLock } from './useLock';
105-
export { useBroadcastChannel } from './useBroadcastChannel';
105+
export { useBroadcastChannel } from './useBroadcastChannel';
106+
export { useStateValidator } from './useStateValidator';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Dispatch, SetStateAction, useRef, useState } from "react";
2+
import { StateValidator } from "../models";
3+
import { useMemoizedFunction } from ".";
4+
5+
/**
6+
* **`useStateValidator`**: custom _useState_ hook that validates state on every update.
7+
* @param {T | () => T} initialState - value or a function.
8+
* @param {StateValidator} validator - function that will be executed to validate state.
9+
* @returns {[T, Dispatch<SetStateAction<T>>, T extends Record<string, unknown> ? {[k in keyof T]:{invalid: boolean, message?: string}} : {invalid: boolean, message?: string}]} invalid
10+
* Array with:
11+
* - first element: __state__ value.
12+
* - second element: __setState__ function to update state.
13+
* - third element: __valid__ validation value/object for state.
14+
*/
15+
export const useStateValidator = <T>(initialState: T | (() => T), validator: StateValidator<T>): [T, Dispatch<SetStateAction<T>>, T extends Record<string, unknown> ? {[k in keyof T]:{invalid: boolean, message?: string}} : {invalid: boolean, message?: string}] => {
16+
const refValidator = useRef<{ [k in keyof T]: { invalid: boolean, message?: string } } | { invalid: boolean, message?: string }>();
17+
const [state, setState] = useState<{ state: T, validation: { [k in keyof T]: { invalid: boolean, message?: string } } | { invalid: boolean, message?: string } }>(() => {
18+
let state;
19+
if (initialState instanceof Function) {
20+
state = initialState();
21+
} else {
22+
state = initialState;
23+
}
24+
let validation: { [k in keyof T]: {invalid: boolean, message?: string} } | {invalid: boolean, message?: string} = {} as { [k in keyof T]: {invalid: boolean, message?: string} };
25+
if (!Array.isArray(state) && !(state instanceof Date) && !(state instanceof RegExp) && typeof state === "object") {
26+
const keys = Reflect.ownKeys(state as Record<string, unknown>);
27+
keys.forEach((key) => {
28+
Reflect.set(validation, key, {invalid: false});
29+
})
30+
} else {
31+
validation = {invalid: false};
32+
}
33+
refValidator.current = validation;
34+
return {
35+
state: state!,
36+
validation: validation
37+
}
38+
});
39+
const update = useMemoizedFunction((val: T | ((value: T) => T)) => {
40+
const newState = val instanceof Function
41+
? val(state.state)
42+
: val;
43+
const validation = validator(newState, JSON.parse(JSON.stringify(refValidator.current)) as T extends Record<string, unknown> ? { [k in keyof T]: { invalid: boolean, message?: string } } : { invalid: boolean, message?: string });
44+
setState({ state: newState, validation: validation });
45+
});
46+
47+
return [state.state, update, state.validation as T extends Record<string,unknown> ? {[k in keyof T]: {invalid: boolean, message?: string}}:{invalid: boolean, message?: string}];
48+
}

packages/react-tools/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ export type {
9191
UseWebWorkerFnProps,
9292
UseWebWorkerFnResult,
9393
ToDataURLOptions,
94-
UseBase64ObjectOptions
94+
UseBase64ObjectOptions,
95+
StateValidator
9596
} from './models'
9697

9798
export {
@@ -199,7 +200,8 @@ export {
199200
usePromiseSuspensible,
200201
useFetch,
201202
useLock,
202-
useBroadcastChannel
203+
useBroadcastChannel,
204+
useStateValidator
203205
} from './hooks'
204206

205207
export {

packages/react-tools/src/models/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ export type { TPermissionName, UsePermissionResult, TPermissionState } from './u
2727
export type { SwipeDirection, UseSwipeProps, UseSwipeResult } from './useSwipe.model';
2828
export type { UseWebWorkerProps, UseWebWorkerResult } from './useWebWorker.model';
2929
export type { UseWebWorkerFnProps, UseWebWorkerFnResult } from './useWebWorkerFn.model';
30-
export type { ToDataURLOptions, UseBase64ObjectOptions } from './getBase64.model';
30+
export type { ToDataURLOptions, UseBase64ObjectOptions } from './getBase64.model';
31+
export type { StateValidator } from './useStateValidator.model';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface StateValidator<T> {
2+
(this: T, state: T, validation: T extends Record<string, unknown> ? {[k in keyof T]: {invalid: boolean, message?: string}}: {invalid: boolean, message?: string}): typeof validation;
3+
(state: T, validation: T extends Record<string, unknown> ? { [k in keyof T]: {invalid: boolean, message?: string} } : {invalid: boolean, message?: string}): typeof validation;
4+
}

0 commit comments

Comments
 (0)