@coxy/react-validator 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/context.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import { createContext } from 'react'
2
2
 
3
- import type { Validity } from './types'
3
+ import type { FieldParams, Validity } from './types'
4
+
5
+ export interface RegisteredFieldHandle {
6
+ props: FieldParams
7
+ validate: () => Validity
8
+ }
4
9
 
5
10
  export const Context = createContext<{
6
- registerField: (field: string | number) => void
7
- unregisterField: (field: string | number) => void
11
+ registerField: (field: RegisteredFieldHandle) => void
12
+ unregisterField: (field: RegisteredFieldHandle) => void
8
13
  customErrors: Array<Validity>
9
14
  }>(null)
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import { act, render } from '@testing-library/react'
6
+ import { createRef } from 'react'
7
+
8
+ import { rules } from './rules'
9
+ import { ValidatorField } from './validator-field'
10
+ import { ValidatorWrapper, type ValidatorWrapper as ValidatorWrapperHandle } from './validator-wrapper'
11
+
12
+ it('setCustomError overrides field validation result and clearCustomErrors restores it', () => {
13
+ const validator = createRef<ValidatorWrapperHandle>()
14
+
15
+ render(
16
+ <ValidatorWrapper ref={validator}>
17
+ <ValidatorField id="email-field" rules={rules.email} value="user@example.com" />
18
+ </ValidatorWrapper>,
19
+ )
20
+
21
+ // Initially valid
22
+ const fieldBefore = validator.current.getField('email-field')
23
+ const validityBefore = fieldBefore.validate()
24
+ expect(validityBefore.isValid).toBe(true)
25
+ expect(validityBefore.message).toBe('')
26
+
27
+ // Set a custom error
28
+ act(() => {
29
+ validator.current.setCustomError({ id: 'email-field', isValid: false, message: 'Custom error' })
30
+ })
31
+
32
+ const fieldWithCustom = validator.current.getField('email-field')
33
+ const validityWithCustom = fieldWithCustom.validate()
34
+ expect(validityWithCustom.isValid).toBe(false)
35
+ expect(validityWithCustom.message).toBe('Custom error')
36
+
37
+ // Clear custom errors
38
+ act(() => {
39
+ validator.current.clearCustomErrors()
40
+ })
41
+ const fieldAfter = validator.current.getField('email-field')
42
+ const validityAfter = fieldAfter.validate()
43
+ expect(validityAfter.isValid).toBe(true)
44
+ expect(validityAfter.message).toBe('')
45
+ })
46
+
47
+ it('custom error is used inside ValidatorField render-prop child', () => {
48
+ const validator = createRef<ValidatorWrapperHandle>()
49
+ const messages: string[] = []
50
+
51
+ render(
52
+ <ValidatorWrapper ref={validator}>
53
+ <ValidatorField id="field-x" rules={rules.password} value="strongpassword">
54
+ {({ message }) => {
55
+ messages.push(message)
56
+ return null
57
+ }}
58
+ </ValidatorField>
59
+ </ValidatorWrapper>,
60
+ )
61
+
62
+ // Initially valid → message pushed should be ''
63
+ expect(messages[messages.length - 1]).toBe('')
64
+
65
+ // After setting custom error, the render-prop should see the custom message.
66
+ act(() => {
67
+ validator.current.setCustomError({ id: 'field-x', isValid: false, message: 'Injected' })
68
+ })
69
+ const field = validator.current.getField('field-x')
70
+ const res = field.validate()
71
+ expect(res.isValid).toBe(false)
72
+ expect(res.message).toBe('Injected')
73
+ })
package/src/index.test.ts CHANGED
@@ -1,7 +1,22 @@
1
- import { ValidatorWrapper, ValidatorField, rules } from './index'
1
+ import { rules, useValidator, Validator, ValidatorField, ValidatorWrapper } from './index'
2
2
 
3
- it('renders with or without a name', () => {
4
- expect(typeof ValidatorWrapper).toBe('function')
5
- expect(typeof ValidatorField).toBe('function')
3
+ it('exports surface is available', () => {
4
+ expect(typeof ValidatorWrapper).toBe('object')
5
+ expect(typeof ValidatorField).toBe('object')
6
6
  expect(typeof rules).toBe('object')
7
+ expect(typeof Validator).toBe('function')
8
+ expect(typeof useValidator).toBe('function')
9
+ })
10
+
11
+ it('use exports to execute a basic validation flow', () => {
12
+ // use Validator (class)
13
+ const validator = new Validator()
14
+ validator.addField({ value: 'test@example.com', rules: rules.email })
15
+ const res = validator.validate()
16
+ expect(res.isValid).toBe(true)
17
+
18
+ // use useValidator (hook-like util function)
19
+ const [isValid, { message }] = useValidator('bad-email', rules.email)
20
+ expect(isValid).toBe(false)
21
+ expect(message).toBe('Email is invalid')
7
22
  })
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { rules } from './rules'
2
+
3
+ export type { ErrorMessage, FieldParams, ValidatorRule, Validity } from './types'
4
+ export { useValidator } from './use-validator'
5
+ export { Validator } from './validator'
2
6
  export { ValidatorField } from './validator-field'
3
7
  export { ValidatorWrapper } from './validator-wrapper'
4
- export { Validator } from './validator'
5
- export { useValidator } from './use-validator'
6
- export * from './types'
package/src/rules.test.ts CHANGED
@@ -33,6 +33,7 @@ it('check rule password', () => {
33
33
  it('check rule bool', () => {
34
34
  expect(rules.bool.length).toBe(1)
35
35
 
36
+ // @ts-expect-error
36
37
  const result = rules.bool[0].rule(true)
37
38
  expect(result).toBe(true)
38
39
  })
@@ -56,18 +57,20 @@ it('check rule min', () => {
56
57
  result = rules.min(10)[0].rule('')
57
58
  expect(result).toBe(false)
58
59
 
59
- result = rules.min(9)[0].rule('testtesttest')
60
+ result = rules.min(9)[0].rule('test-test-test')
60
61
  expect(result).toBe(false)
61
62
 
62
63
  result = rules.min(9)[0].rule('11')
63
64
  expect(result).toBe(true)
64
65
 
66
+ // @ts-expect-error
65
67
  result = rules.min(9)[0].rule(10)
66
68
  expect(result).toBe(true)
67
69
 
68
70
  result = rules.min(9)[0].rule('8')
69
71
  expect(result).toBe(false)
70
72
 
73
+ // @ts-expect-error
71
74
  result = rules.min(9)[0].rule(7)
72
75
  expect(result).toBe(false)
73
76
 
@@ -80,18 +83,20 @@ it('check rule max', () => {
80
83
  result = rules.max(10)[0].rule('')
81
84
  expect(result).toBe(false)
82
85
 
83
- result = rules.max(9)[0].rule('testtesttest')
86
+ result = rules.max(9)[0].rule('test-test-test')
84
87
  expect(result).toBe(false)
85
88
 
86
89
  result = rules.max(9)[0].rule('11')
87
90
  expect(result).toBe(false)
88
91
 
92
+ // @ts-expect-error
89
93
  result = rules.max(9)[0].rule(10)
90
94
  expect(result).toBe(false)
91
95
 
92
96
  result = rules.max(9)[0].rule('5')
93
97
  expect(result).toBe(true)
94
98
 
99
+ // @ts-expect-error
95
100
  result = rules.max(9)[0].rule(5)
96
101
  expect(result).toBe(true)
97
102
 
@@ -107,10 +112,10 @@ it('check rule length', () => {
107
112
  result = rules.length(1)[0].rule('1')
108
113
  expect(result).toBe(true)
109
114
 
110
- result = rules.length(1, 10)[0].rule('testtesttest')
115
+ result = rules.length(1, 10)[0].rule('test-test-test')
111
116
  expect(result).toBe(true)
112
117
 
113
- result = rules.length(1, 10)[1].rule('testtesttest')
118
+ result = rules.length(1, 10)[1].rule('test-test-test')
114
119
  expect(result).toBe(false)
115
120
 
116
121
  result = rules.length(1, 10)[0].rule('lol')
package/src/rules.ts CHANGED
@@ -2,68 +2,68 @@ import type { ValidatorRule } from './types'
2
2
 
3
3
  // eslint-disable-next-line
4
4
  const emailReg =
5
- /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
5
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
6
6
 
7
7
  export type ValidatorRules = ValidatorRule[]
8
8
 
9
9
  export const rules = {
10
10
  notEmpty: [
11
11
  {
12
- rule: (value) => value !== '' && value.length > 0,
12
+ rule: (value: string) => value !== '' && value.length > 0,
13
13
  message: 'Value is required',
14
14
  },
15
15
  ],
16
16
 
17
17
  bool: [
18
18
  {
19
- rule: (value) => !!value,
19
+ rule: (value: string) => !!value,
20
20
  message: 'Value is required',
21
21
  },
22
22
  ],
23
23
 
24
24
  password: [
25
25
  {
26
- rule: (value) => value.length > 0,
26
+ rule: (value: string) => value.length > 0,
27
27
  message: 'Password field cannot be empty',
28
28
  },
29
29
  {
30
- rule: (value) => value.length > 5,
30
+ rule: (value: string) => value.length > 5,
31
31
  message: 'Password field can not be less than 6 characters',
32
32
  },
33
33
  ],
34
34
 
35
35
  email: [
36
36
  {
37
- rule: (value) => !!value && value !== '' && value.length !== 0,
37
+ rule: (value: string) => !!value && value !== '' && value.length !== 0,
38
38
  message: 'Email is required',
39
39
  },
40
40
  {
41
- rule: (value) => emailReg.test(String(value).toLowerCase()),
41
+ rule: (value: string) => emailReg.test(String(value).toLowerCase()),
42
42
  message: 'Email is invalid',
43
43
  },
44
44
  ],
45
45
 
46
- min: (min) => [
46
+ min: (min: number) => [
47
47
  {
48
- rule: (value) => Number.parseFloat(value) > min,
48
+ rule: (value: string) => Number.parseFloat(value) > min,
49
49
  message: `The value must be greater than ${min}`,
50
50
  },
51
51
  ],
52
52
 
53
- max: (max) => [
53
+ max: (max: number) => [
54
54
  {
55
- rule: (value) => Number.parseFloat(value) < max,
55
+ rule: (value: string) => Number.parseFloat(value) < max,
56
56
  message: `The value must be smaller ${max}`,
57
57
  },
58
58
  ],
59
59
 
60
- length: (min, max?) => [
60
+ length: (min: number, max?: number) => [
61
61
  {
62
- rule: (value) => String(value).length >= min,
62
+ rule: (value: string) => String(value).length >= min,
63
63
  message: `No less than ${min} symbols`,
64
64
  },
65
65
  {
66
- rule: (value) => (max !== undefined ? String(value).length <= max : true),
66
+ rule: (value: string) => (max !== undefined ? String(value).length <= max : true),
67
67
  message: `No more than ${max} symbols`,
68
68
  },
69
69
  ],
@@ -2,8 +2,7 @@
2
2
  * @jest-environment jsdom
3
3
  */
4
4
 
5
- import { render, screen } from '@testing-library/react'
6
- import { act } from '@testing-library/react'
5
+ import { act, render, screen } from '@testing-library/react'
7
6
  import { useEffect, useState } from 'react'
8
7
 
9
8
  import { rules } from './rules'
@@ -21,7 +21,7 @@ it('check context validator', () => {
21
21
  const validator = createRef<ValidatorWrapper>()
22
22
  render(
23
23
  <ValidatorWrapper ref={validator}>
24
- <ValidatorField rules={[]} />
24
+ <ValidatorField rules={[]} value="" />
25
25
  </ValidatorWrapper>,
26
26
  )
27
27
 
@@ -130,8 +130,8 @@ it('check custom rule message function', () => {
130
130
  const validator = createRef<ValidatorWrapper>()
131
131
  const rule = [
132
132
  {
133
- rule: (value) => value !== 'test',
134
- message: (value) => `test message ${value}`,
133
+ rule: (value: string) => value !== 'test',
134
+ message: (value: string) => `test message ${value}`,
135
135
  },
136
136
  ]
137
137
  render(
@@ -147,3 +147,34 @@ it('check custom rule message function', () => {
147
147
  expect(validateResult.isValid).toBe(false)
148
148
  expect(validateResult.message).toBe('test message test')
149
149
  })
150
+
151
+ jest.useFakeTimers()
152
+
153
+ it('re-renders the same field to cover handleRef initialization false branch and else-validate path', () => {
154
+ const validator = createRef<ValidatorWrapper>()
155
+
156
+ function Comp() {
157
+ const [val, setVal] = useState('')
158
+ useEffect(() => {
159
+ setTimeout(() => {
160
+ act(() => setVal('abc'))
161
+ }, 50)
162
+ }, [])
163
+ return (
164
+ <ValidatorWrapper ref={validator}>
165
+ <ValidatorField id="rerender" rules={rules.notEmpty} value={val}>
166
+ {() => null}
167
+ </ValidatorField>
168
+ </ValidatorWrapper>
169
+ )
170
+ }
171
+
172
+ render(<Comp />)
173
+ // initial: invalid
174
+ let field = validator.current.getField('rerender')
175
+ expect(field.validate().isValid).toBe(false)
176
+
177
+ jest.runAllTimers()
178
+ field = validator.current.getField('rerender')
179
+ expect(field.validate().isValid).toBe(true)
180
+ })
@@ -1,69 +1,59 @@
1
- import { Component, type ReactNode } from 'react'
2
- import type { Validity } from 'types'
1
+ import { forwardRef, type ReactNode, useContext, useEffect, useRef } from 'react'
2
+ import type { FieldParams, Validity } from 'types'
3
3
 
4
- import { Context } from './context'
5
- import type { ValidatorRules } from './rules'
4
+ import { Context, type RegisteredFieldHandle } from './context'
6
5
  import { Field } from './validator'
7
6
 
8
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
7
+ // biome-ignore lint/suspicious/noExplicitAny: <need>
9
8
  export type Value = any
10
9
 
11
10
  type Fn = (validity: Validity, value: Value) => ReactNode
12
11
 
13
- interface Props {
14
- rules?: ValidatorRules
15
- required?: boolean
16
- value?: Value
17
- id?: string | number
12
+ type Props = FieldParams & {
18
13
  children?: ReactNode | Fn
19
- unregisterField: (val: Value) => void
20
- registerField: (val: Value) => void
21
- customErrors: Array<Validity>
22
14
  }
23
15
 
24
- class ValidationFieldWrapper extends Component<Props> {
25
- componentWillUnmount() {
26
- this.props.unregisterField(this)
27
- }
28
-
29
- componentDidMount() {
30
- this.props.registerField(this)
16
+ export const ValidatorField = forwardRef<unknown, Props>(function ValidatorField(props: Props, _ref) {
17
+ const { children, value } = props
18
+ const { customErrors, registerField, unregisterField } = useContext(Context)
19
+
20
+ const propsRef = useRef(props)
21
+ propsRef.current = props
22
+
23
+ const customErrorsRef = useRef(customErrors)
24
+ customErrorsRef.current = customErrors
25
+
26
+ const handleRef = useRef<RegisteredFieldHandle | null>(null)
27
+ if (!handleRef.current) {
28
+ handleRef.current = {
29
+ get props() {
30
+ return propsRef.current
31
+ },
32
+ validate: () => {
33
+ const curr = propsRef.current
34
+ const customError = customErrorsRef.current.find((item) => item.id === curr.id)
35
+ if (customError) {
36
+ return customError
37
+ }
38
+ const field = new Field({
39
+ rules: curr.rules,
40
+ required: curr.required,
41
+ value: curr.value,
42
+ id: curr.id,
43
+ })
44
+ return field.validate()
45
+ },
46
+ }
31
47
  }
32
48
 
33
- validate(): Validity {
34
- const props = this.props
35
- const customError = props.customErrors.find((item) => item.id === props.id)
36
- if (customError) {
37
- return customError
49
+ useEffect(() => {
50
+ registerField(handleRef.current as RegisteredFieldHandle)
51
+ return () => {
52
+ unregisterField(handleRef.current as RegisteredFieldHandle)
38
53
  }
54
+ }, [registerField, unregisterField])
39
55
 
40
- const field = new Field({
41
- rules: props.rules,
42
- required: props.required,
43
- value: props.value,
44
- id: props.id,
45
- })
46
- return field.validate()
47
- }
48
-
49
- render() {
50
- const { children, value } = this.props
51
- const validity = this.validate()
52
- return typeof children === 'function' ? children(validity, value) : children
53
- }
54
- }
56
+ const validity = handleRef.current.validate()
55
57
 
56
- export function ValidatorField(props: Omit<Props, 'registerField' | 'unregisterField' | 'customErrors'>) {
57
- return (
58
- <Context.Consumer>
59
- {(data) => (
60
- <ValidationFieldWrapper
61
- {...props}
62
- customErrors={data.customErrors}
63
- registerField={data.registerField}
64
- unregisterField={data.unregisterField}
65
- />
66
- )}
67
- </Context.Consumer>
68
- )
69
- }
58
+ return typeof children === 'function' ? (children as Fn)(validity, value) : (children as ReactNode)
59
+ })
@@ -13,12 +13,12 @@ it('check wrapper validator', () => {
13
13
  const validator = createRef<ValidatorWrapper>()
14
14
  render(
15
15
  <ValidatorWrapper ref={validator}>
16
- <ValidatorField rules={[]} />
17
- <ValidatorField rules={[]} />
18
- <ValidatorField rules={[]} />
19
- <ValidatorField rules={[]} />
20
- <ValidatorField rules={[]} />
21
- <ValidatorField rules={[]} />
16
+ <ValidatorField rules={[]} value="" />
17
+ <ValidatorField rules={[]} value="" />
18
+ <ValidatorField rules={[]} value="" />
19
+ <ValidatorField rules={[]} value="" />
20
+ <ValidatorField rules={[]} value="" />
21
+ <ValidatorField rules={[]} value="" />
22
22
  </ValidatorWrapper>,
23
23
  )
24
24
 
@@ -30,8 +30,8 @@ it('check getField validator', () => {
30
30
  const validator = createRef<ValidatorWrapper>()
31
31
  render(
32
32
  <ValidatorWrapper ref={validator}>
33
- <ValidatorField rules={[]} id="test" />
34
- <ValidatorField rules={[]} id="test-fields" />
33
+ <ValidatorField rules={[]} id="test" value="" />
34
+ <ValidatorField rules={[]} id="test-fields" value="" />
35
35
  </ValidatorWrapper>,
36
36
  )
37
37
  expect(typeof validator.current.getField).toBe('function')
@@ -48,7 +48,7 @@ it('check getField undefined field', () => {
48
48
  const validator = createRef<ValidatorWrapper>()
49
49
  render(
50
50
  <ValidatorWrapper ref={validator}>
51
- <ValidatorField rules={[]} id="test-empty-field" />
51
+ <ValidatorField rules={[]} id="test-empty-field" value="" />
52
52
  </ValidatorWrapper>,
53
53
  )
54
54
 
@@ -75,7 +75,7 @@ it('check unregisterField, registerField', () => {
75
75
  const validator = createRef<ValidatorWrapper>()
76
76
  render(
77
77
  <ValidatorWrapper ref={validator}>
78
- <ValidatorField rules={[]} id="test-register-field" />
78
+ <ValidatorField rules={[]} id="test-register-field" value="" />
79
79
  </ValidatorWrapper>,
80
80
  )
81
81
 
@@ -87,9 +87,9 @@ it('check filed in field', () => {
87
87
  const validator = createRef<ValidatorWrapper>()
88
88
  render(
89
89
  <ValidatorWrapper ref={validator}>
90
- <ValidatorField rules={[]}>
91
- <ValidatorField rules={[]} id="check-validate-field-1" />
92
- <ValidatorField rules={[]} id="check-validate-field-2" />
90
+ <ValidatorField rules={[]} value="">
91
+ <ValidatorField rules={[]} id="check-validate-field-1" value="" />
92
+ <ValidatorField rules={[]} id="check-validate-field-2" value="" />
93
93
  </ValidatorField>
94
94
  </ValidatorWrapper>,
95
95
  )
@@ -132,3 +132,21 @@ it('check two validators', () => {
132
132
  expect(validatorFailed.current.validate().isValid).toBe(false)
133
133
  expect(validatorSuccess.current.validate().isValid).toBe(true)
134
134
  })
135
+
136
+ it('covers registerField duplicate and unregisterField non-existing branches', () => {
137
+ const validator = createRef<ValidatorWrapper>()
138
+ render(
139
+ <ValidatorWrapper ref={validator}>
140
+ <ValidatorField rules={[]} id="dup-field" value="" />
141
+ </ValidatorWrapper>,
142
+ )
143
+
144
+ const handle = validator.current.getField('dup-field')
145
+ validator.current.registerField(handle)
146
+ validator.current.unregisterField(handle)
147
+ const dummy = {
148
+ props: { value: '', rules: [], id: 'dummy' },
149
+ validate: () => ({ isValid: true, message: '' }),
150
+ }
151
+ validator.current.unregisterField(dummy)
152
+ })
@@ -1,75 +1,78 @@
1
- import { Component, type ReactNode, type RefObject } from 'react'
1
+ import { forwardRef, type ReactNode, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'
2
2
 
3
- import { Context } from './context'
3
+ import { Context, type RegisteredFieldHandle } from './context'
4
4
  import type { Validity } from './types'
5
- import { type Field, Validator } from './validator'
5
+ import { Validator } from './validator'
6
6
 
7
7
  interface ComponentProps {
8
8
  children?: ReactNode
9
9
  stopAtFirstError?: boolean
10
- ref?: RefObject<ValidatorWrapper>
11
10
  }
12
11
 
13
- export class ValidatorWrapper extends Component<ComponentProps> {
14
- fields = []
15
- state = {
16
- customErrors: [],
17
- }
18
-
19
- constructor(props) {
20
- super(props)
21
- this.registerField = this.registerField.bind(this)
22
- this.unregisterField = this.unregisterField.bind(this)
23
- }
12
+ export interface ValidatorWrapper {
13
+ validate: () => Validity
14
+ getField: (id: string | number) => RegisteredFieldHandle | null
15
+ registerField: (field: RegisteredFieldHandle) => void
16
+ unregisterField: (field: RegisteredFieldHandle) => void
17
+ setCustomError: (customError: Validity) => void
18
+ clearCustomErrors: () => void
19
+ }
24
20
 
25
- componentWillUnmount() {
26
- this.fields = []
27
- }
21
+ export const ValidatorWrapper = forwardRef<ValidatorWrapper, ComponentProps>(function ValidatorWrapper(
22
+ { children, stopAtFirstError },
23
+ ref,
24
+ ) {
25
+ const fieldsRef = useRef<RegisteredFieldHandle[]>([])
26
+ const [customErrors, setCustomErrors] = useState<Validity[]>([])
28
27
 
29
- registerField(field) {
30
- if (field && !this.fields.includes(field)) {
31
- this.fields.push(field)
28
+ const registerField = useCallback((field: RegisteredFieldHandle) => {
29
+ if (field && !fieldsRef.current.includes(field)) {
30
+ fieldsRef.current.push(field)
32
31
  }
33
- }
32
+ }, [])
34
33
 
35
- unregisterField(field) {
36
- const index = this.fields.indexOf(field)
37
- if (index > -1) this.fields.splice(index, 1)
38
- }
34
+ const unregisterField = useCallback((field: RegisteredFieldHandle) => {
35
+ const index = fieldsRef.current.indexOf(field)
36
+ if (index > -1) fieldsRef.current.splice(index, 1)
37
+ }, [])
39
38
 
40
- getField(id): Field | null {
41
- return this.fields.find((field) => field.props.id === id) || null
42
- }
39
+ const getField = useCallback<ValidatorWrapper['getField']>((id) => {
40
+ return fieldsRef.current.find((field) => field?.props?.id === id) || null
41
+ }, [])
43
42
 
44
- setCustomError(customError: Validity) {
45
- this.setState({
46
- customErrors: [...this.state.customErrors, customError],
47
- })
48
- }
43
+ const setCustomError = useCallback<ValidatorWrapper['setCustomError']>((customError) => {
44
+ setCustomErrors((prev) => [...prev, customError])
45
+ }, [])
49
46
 
50
- clearCustomErrors() {
51
- this.setState({ customErrors: [] })
52
- }
47
+ const clearCustomErrors = useCallback<ValidatorWrapper['clearCustomErrors']>(() => {
48
+ setCustomErrors([])
49
+ }, [])
53
50
 
54
- validate(): Validity {
55
- const validator = new Validator({ stopAtFirstError: this.props.stopAtFirstError })
56
- for (const comp of this.fields) {
51
+ const validate = useCallback<ValidatorWrapper['validate']>(() => {
52
+ const validator = new Validator({ stopAtFirstError })
53
+ for (const comp of fieldsRef.current) {
57
54
  validator.addField(comp.props)
58
55
  }
59
56
  return validator.validate()
60
- }
57
+ }, [stopAtFirstError])
61
58
 
62
- render(): ReactNode {
63
- return (
64
- <Context.Provider
65
- value={{
66
- customErrors: this.state.customErrors,
67
- registerField: this.registerField,
68
- unregisterField: this.unregisterField,
69
- }}
70
- >
71
- {this.props.children}
72
- </Context.Provider>
73
- )
74
- }
75
- }
59
+ useImperativeHandle(
60
+ ref,
61
+ () => ({
62
+ validate,
63
+ getField,
64
+ registerField,
65
+ unregisterField,
66
+ setCustomError,
67
+ clearCustomErrors,
68
+ }),
69
+ [validate, getField, registerField, unregisterField, setCustomError, clearCustomErrors],
70
+ )
71
+
72
+ const contextValue = useMemo(
73
+ () => ({ customErrors, registerField, unregisterField }),
74
+ [customErrors, registerField, unregisterField],
75
+ )
76
+
77
+ return <Context.Provider value={contextValue}>{children}</Context.Provider>
78
+ })