Skip to content

Commit e2a7bcd

Browse files
authored
Change all uses of RHF <Controller> to useController (#2102)
change all uses of Controller to useController
1 parent 56a0048 commit e2a7bcd

File tree

7 files changed

+165
-195
lines changed

7 files changed

+165
-195
lines changed

.eslintrc.cjs

+4-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ module.exports = {
4141
'@typescript-eslint/no-empty-interface': 'off',
4242
'@typescript-eslint/ban-ts-comment': 'off',
4343
'@typescript-eslint/no-non-null-assertion': 'off',
44-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
44+
'@typescript-eslint/no-unused-vars': [
45+
'error',
46+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
47+
],
4548
eqeqeq: ['error', 'always', { null: 'ignore' }],
4649
'import/no-default-export': 'error',
4750
'import/no-unresolved': 'off', // plugin doesn't know anything

app/components/form/fields/CheckboxField.tsx

+37-33
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { Controller, type Control, type FieldPath, type FieldValues } from 'react-hook-form'
8+
import {
9+
useController,
10+
type Control,
11+
type FieldPath,
12+
type FieldValues,
13+
} from 'react-hook-form'
914

1015
import { Checkbox, type CheckboxProps } from '~/ui/lib/Checkbox'
1116

@@ -24,35 +29,34 @@ export const CheckboxField = <
2429
control,
2530
name,
2631
...props
27-
}: CheckboxFieldProps<TFieldValues, TName>) => (
28-
<Controller
29-
name={name}
30-
control={control}
31-
render={({ field: { onChange, value } }) => (
32-
<Checkbox
33-
{...props}
34-
// If value is an array, we're dealing with a set of checkboxes that
35-
// have the same name and different `value` attrs, and are therefore
36-
// supposed to produce an array of the values that are checked. `value`
37-
// is the value in form state, which can be a bool or array.
38-
// `props.value` is the value string of the current checkbox, which is
39-
// only relevant in the array case
40-
onChange={(e) => {
41-
if (Array.isArray(value) && props.value) {
42-
// it's one of a set of checkboxes. if it was just checked, we're
43-
// adding it to the array, otherwise we're removing it
44-
const valueArray = value as string[]
45-
const newValue = e.target.checked
46-
? [...valueArray, props.value]
47-
: valueArray.filter((x) => x !== props.value)
48-
onChange(newValue)
49-
} else {
50-
// it's a single checkbox
51-
onChange(e.target.checked)
52-
}
53-
}}
54-
checked={Array.isArray(value) ? value.includes(props.value) : value}
55-
/>
56-
)}
57-
/>
58-
)
32+
}: CheckboxFieldProps<TFieldValues, TName>) => {
33+
const {
34+
field: { onChange, value },
35+
} = useController({ name, control })
36+
return (
37+
<Checkbox
38+
{...props}
39+
// If value is an array, we're dealing with a set of checkboxes that
40+
// have the same name and different `value` attrs, and are therefore
41+
// supposed to produce an array of the values that are checked. `value`
42+
// is the value in form state, which can be a bool or array.
43+
// `props.value` is the value string of the current checkbox, which is
44+
// only relevant in the array case
45+
onChange={(e) => {
46+
if (Array.isArray(value) && props.value) {
47+
// it's one of a set of checkboxes. if it was just checked, we're
48+
// adding it to the array, otherwise we're removing it
49+
const valueArray = value as string[]
50+
const newValue = e.target.checked
51+
? [...valueArray, props.value]
52+
: valueArray.filter((x) => x !== props.value)
53+
onChange(newValue)
54+
} else {
55+
// it's a single checkbox
56+
onChange(e.target.checked)
57+
}
58+
}}
59+
checked={Array.isArray(value) ? value.includes(props.value) : value}
60+
/>
61+
)
62+
}

app/components/form/fields/FileField.tsx

+27-32
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { Controller, type Control, type FieldPath, type FieldValues } from 'react-hook-form'
8+
import {
9+
useController,
10+
type Control,
11+
type FieldPath,
12+
type FieldValues,
13+
} from 'react-hook-form'
914

1015
import { FieldLabel } from '~/ui/lib/FieldLabel'
1116
import { FileInput } from '~/ui/lib/FileInput'
@@ -37,37 +42,27 @@ export function FileField<
3742
description?: string | React.ReactNode
3843
disabled?: boolean
3944
}) {
45+
const {
46+
field: { value: _, ...rest },
47+
fieldState: { error },
48+
} = useController({ name, control, rules: { required } })
4049
return (
41-
<Controller
42-
name={name}
43-
control={control}
44-
rules={{ required }}
45-
render={({ field: { value: _value, ...rest }, fieldState: { error } }) => (
46-
<div>
47-
<div className="mb-2">
48-
<FieldLabel
49-
id={`${id}-label`}
50-
htmlFor={id}
51-
tip={tooltipText}
52-
optional={!required}
53-
>
54-
{label}
55-
</FieldLabel>
56-
{description && (
57-
<TextInputHint id={`${id}-help-text`}>{description}</TextInputHint>
58-
)}
59-
</div>
60-
<FileInput
61-
id={id}
62-
className="mt-2"
63-
accept={accept}
64-
disabled={disabled}
65-
{...rest}
66-
error={!!error}
67-
/>
68-
<ErrorMessage error={error} label={label} />
69-
</div>
70-
)}
71-
/>
50+
<div>
51+
<div className="mb-2">
52+
<FieldLabel id={`${id}-label`} htmlFor={id} tip={tooltipText} optional={!required}>
53+
{label}
54+
</FieldLabel>
55+
{description && <TextInputHint id={`${id}-help-text`}>{description}</TextInputHint>}
56+
</div>
57+
<FileInput
58+
id={id}
59+
className="mt-2"
60+
accept={accept}
61+
disabled={disabled}
62+
{...rest}
63+
error={!!error}
64+
/>
65+
<ErrorMessage error={error} label={label} />
66+
</div>
7267
)
7368
}

app/components/form/fields/ListboxField.tsx

+25-28
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
* Copyright Oxide Computer Company
77
*/
88
import cn from 'classnames'
9-
import { Controller, type Control, type FieldPath, type FieldValues } from 'react-hook-form'
9+
import {
10+
useController,
11+
type Control,
12+
type FieldPath,
13+
type FieldValues,
14+
} from 'react-hook-form'
1015

1116
import { Listbox, type ListboxItem } from '~/ui/lib/Listbox'
1217
import { capitalize } from '~/util/str'
@@ -50,37 +55,29 @@ export function ListboxField<
5055
}: ListboxFieldProps<TFieldValues, TName>) {
5156
// TODO: recreate this logic
5257
// validate: (v) => (required && !v ? `${name} is required` : undefined),
58+
const { field, fieldState } = useController({ name, control, rules: { required } })
5359
return (
5460
<div className={cn('max-w-lg', className)}>
55-
<Controller
61+
<Listbox
62+
description={description}
63+
label={label}
64+
tooltipText={tooltipText}
65+
required={required}
66+
placeholder={placeholder}
67+
selected={field.value || null}
68+
items={items}
69+
onChange={(value) => {
70+
field.onChange(value)
71+
onChange?.(value)
72+
}}
73+
// required to get required error to trigger on blur
74+
// onBlur={field.onBlur}
75+
disabled={disabled}
5676
name={name}
57-
rules={{ required }}
58-
control={control}
59-
render={({ field, fieldState: { error } }) => (
60-
<>
61-
<Listbox
62-
description={description}
63-
label={label}
64-
tooltipText={tooltipText}
65-
required={required}
66-
placeholder={placeholder}
67-
selected={field.value || null}
68-
items={items}
69-
onChange={(value) => {
70-
field.onChange(value)
71-
onChange?.(value)
72-
}}
73-
// required to get required error to trigger on blur
74-
// onBlur={field.onBlur}
75-
disabled={disabled}
76-
name={name}
77-
hasError={error !== undefined}
78-
isLoading={isLoading}
79-
/>
80-
<ErrorMessage error={error} label={label} />
81-
</>
82-
)}
77+
hasError={fieldState.error !== undefined}
78+
isLoading={isLoading}
8379
/>
80+
<ErrorMessage error={fieldState.error} label={label} />
8481
</div>
8582
)
8683
}

app/components/form/fields/NumberField.tsx

+20-28
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import cn from 'classnames'
99
import { useId } from 'react'
10-
import { Controller, type FieldPathByValue, type FieldValues } from 'react-hook-form'
10+
import { useController, type FieldPathByValue, type FieldValues } from 'react-hook-form'
1111

1212
import { FieldLabel } from '~/ui/lib/FieldLabel'
1313
import { NumberInput } from '~/ui/lib/NumberInput'
@@ -77,33 +77,25 @@ export const NumberFieldInner = <
7777
const generatedId = useId()
7878
const id = idProp || generatedId
7979

80+
const {
81+
field,
82+
fieldState: { error },
83+
} = useController({ name, control, rules: { required, validate } })
84+
8085
return (
81-
<Controller
82-
name={name}
83-
control={control}
84-
rules={{ required, validate }}
85-
render={({ field, fieldState: { error } }) => {
86-
return (
87-
<>
88-
<NumberInput
89-
id={id}
90-
error={!!error}
91-
aria-labelledby={cn(`${id}-label`, {
92-
[`${id}-help-text`]: !!tooltipText,
93-
})}
94-
aria-describedby={tooltipText ? `${id}-label-tip` : undefined}
95-
isDisabled={disabled}
96-
maxValue={max ? Number(max) : undefined}
97-
minValue={min !== undefined ? Number(min) : undefined}
98-
{...field}
99-
formatOptions={{
100-
useGrouping: false,
101-
}}
102-
/>
103-
<ErrorMessage error={error} label={label} />
104-
</>
105-
)
106-
}}
107-
/>
86+
<>
87+
<NumberInput
88+
id={id}
89+
error={!!error}
90+
aria-labelledby={cn(`${id}-label`, !!tooltipText && `${id}-help-text`)}
91+
aria-describedby={tooltipText ? `${id}-label-tip` : undefined}
92+
isDisabled={disabled}
93+
maxValue={max ? Number(max) : undefined}
94+
minValue={min !== undefined ? Number(min) : undefined}
95+
{...field}
96+
formatOptions={{ useGrouping: false }}
97+
/>
98+
<ErrorMessage error={error} label={label} />
99+
</>
108100
)
109101
}

0 commit comments

Comments
 (0)