useController
</> useController: (UseControllerProps) => UseControllerReturn
이 사용자 정의 훅은 Controller의 기반으로 동작하며 Controller와 같은 속성 및 메서드를 공유합니다. 재사용 가능한 제어 입력을 만들 때 유용합니다.
매개변수
다음 표는 useController의 인수에 관한 정보를 제공합니다.
| 이름 | 타입 | 필수 | 설명 |
|---|---|---|---|
name | FieldPath | ✓ | 입력의 고유한 이름입니다. 반응형으로 동작하므로 이 속성이 변경되면 controller가 다시 구독하며, 필드 이름을 동적으로 전환할 수 있습니다. |
control | Control | control 객체는 useForm을 호출하면 제공됩니다. FormProvider를 사용할 때는 선택 사항입니다. | |
rules | Object | register와 같은 형식의 검증 규칙입니다. required, min, max, minLength, maxLength, pattern, validate를 포함합니다.rules={{ required: true }} | |
shouldUnregister | boolean = false | 입력이 언마운트되면 등록이 해제되고 defaultValues도 제거됩니다. 참고: useFieldArray와 함께 사용할 때는 입력 언마운트/리마운트 및 순서 변경 후 unregister 함수가 호출되므로 이 속성을 사용하지 않는 것이 좋습니다. | |
disabled | boolean = false | v7.46.0부터 disabled 속성이 field 속성에서 반환됩니다. 제어 입력이 비활성화되고 해당 값은 제출 데이터에서 제외됩니다. | |
defaultValue | unknown | 중요: undefined는 defaultValue 또는 defaultValues에 useForm에서 적용할 수 없습니다.
| |
exact | boolean = true | v7.68.0부터 입력 이름 구독의 정확한 일치를 활성화하며 기본값은 true입니다. v7.81.0부터 exact: true이면 상위 경로 중 하나가 설정될 때도 필드가 갱신됩니다(예: 이름이 "users.0.name"인 필드에는 setValue("users.0", { name: "Jane" })가 반영됨). 하지만 값의 하위 경로가 변경될 때는 갱신되지 않습니다(예: setValue("users.0.name", "Jane")은 이름이 "users.0"인 필드를 갱신하지 않음). 중첩된 갱신도 받으려면 exact: false를 전달합니다. 참고: useWatch와 useFormState는 모두 exact의 기본값이 false이므로 이 동작과 다릅니다. |
반환값
다음 표는 useController가 반환하는 속성에 관한 정보를 제공합니다.
| 객체 이름 | 이름 | 타입 | 설명 |
|---|---|---|---|
field | onChange | (value: any) => void | 입력 값을 라이브러리에 전달하는 함수입니다. 입력의 onChange 속성에 할당해야 하며 값은 undefined가 아니어야 합니다. 이 속성은 formState를 갱신하므로 setValue나 필드 갱신과 관련된 다른 API를 직접 호출하지 않는 것이 좋습니다. |
field | onBlur | () => void | 입력의 onBlur 이벤트를 라이브러리에 전달하는 함수입니다. 입력의 onBlur 속성에 할당해야 합니다. |
field | value | unknown | 제어 컴포넌트의 현재 값입니다. |
field | disabled | boolean | 입력의 비활성화 상태입니다. |
field | name | string | 등록 중인 입력의 이름입니다. |
field | ref | React.Ref | React Hook Form을 입력에 연결하는 ref입니다. 오류가 있는 입력에 React Hook Form이 포커스를 설정할 수 있도록 컴포넌트의 입력 ref에 ref를 할당합니다. |
fieldState | invalid | boolean | 현재 입력의 invalid 상태입니다. |
fieldState | isTouched | boolean | 현재 제어 입력의 touched 상태입니다. |
fieldState | isDirty | boolean | 현재 제어 입력의 dirty 상태입니다. |
fieldState | error | object | v7.0.0부터 이 입력의 오류입니다. |
formState | isDirty | boolean | 사용자가 입력 중 하나라도 수정하면 true로 설정됩니다. 중요: 모든 입력의 defaultValues를 useForm에 제공하여 React Hook Form이 폼의 dirty 상태를 비교할 단일 기준을 갖게 해야 합니다. |
formState | dirtyFields | object | 사용자가 수정한 필드를 담은 객체입니다. 라이브러리가 defaultValues와 비교할 수 있도록 useForm에 모든 입력의 defaultValues를 제공해야 합니다. |
formState | touchedFields | object | 사용자가 상호작용한 모든 입력을 담은 객체입니다. |
formState | defaultValues | object | v7.37.0부터 useForm의 defaultValues에 설정했거나 defaultValues를 reset API로 갱신한 값입니다. |
formState | isSubmitted | boolean | 폼이 제출되면 true로 설정되고, true 상태는 reset 메서드를 호출할 때까지 유지됩니다. |
formState | isSubmitSuccessful | boolean | 런타임 오류 없이 폼이 성공적으로 제출되었음을 나타냅니다. |
formState | isSubmitting | boolean | 현재 폼을 제출 중이면 true, 그렇지 않으면 false입니다. |
formState | isLoading | boolean | v7.41.0부터 현재 폼이 비동기 기본값을 불러오는 중이면 true입니다. 중요: 이 속성은 비동기 defaultValues에만 적용됩니다. |
formState | submitCount | number | 폼이 제출된 횟수입니다. |
formState | isValid | boolean | 폼에 오류가 없으면 true로 설정됩니다. setError는 즉시 isValid를 false로 강제합니다. 이 값 자체는 검증에서 파생되지 않으며, 다음 검증이 실행될 때(예: 다음 onChange, 제출 또는 trigger() 호출) 덮어쓰입니다. |
formState | isValidating | boolean | 검증 중에는 true로 설정됩니다. |
formState | validatingFields | object | v7.51.0부터 비동기 검증이 진행 중인 필드를 담습니다. |
formState | errors | object | 필드 오류를 담은 객체입니다. 오류 메시지를 쉽게 가져올 수 있는 ErrorMessage 컴포넌트도 있습니다. |
formState | disabled | boolean | v7.48.0부터 true로 설정되는 경우는 disabled 속성을 useForm에 전달해 폼을 비활성화했을 때입니다. |
formState | isReady | boolean | v7.56.0부터 formState 구독 설정이 준비되면 true로 설정됩니다. |
예제
import { TextField } from "@mui/material"
import { useController, useForm } from "react-hook-form"
function Input({ control, name }) {
const {
field,
fieldState: { invalid, isTouched, isDirty },
formState: { touchedFields, dirtyFields },
} = useController({
name,
control,
rules: { required: true },
})
return (
<TextField
onChange={field.onChange} // send the value to hook form
onBlur={field.onBlur} // notify when the input is touched or blurred
value={field.value} // input value
name={field.name} // send the input name
inputRef={field.ref} // send input ref, so we can focus on the input when an error appears
/>
)
}
import { useForm, useController, UseControllerProps } from "react-hook-form"
type FormValues = {
FirstName: string
}
function Input(props: UseControllerProps<FormValues>) {
const { field, fieldState } = useController(props)
return (
<div>
<input {...field} placeholder={props.name} />
<p>{fieldState.isTouched && "Touched"}</p>
<p>{fieldState.isDirty && "Dirty"}</p>
<p>{fieldState.invalid ? "invalid" : "valid"}</p>
</div>
)
}
export default function App() {
const { handleSubmit, control } = useForm<FormValues>({
defaultValues: {
FirstName: "",
},
mode: "onChange",
})
const onSubmit = (data: FormValues) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input control={control} name="FirstName" rules={{ required: true }} />
<input type="submit" />
</form>
)
}
import { useState } from "react"
import { useController, useForm } from "react-hook-form"
const Checkboxes = ({ options, control, name }) => {
const { field } = useController({
control,
name,
})
const [value, setValue] = useState(field.value || [])
return (
<>
{options.map((option, index) => (
<input
onChange={(e) => {
const valueCopy = [...value]
// update checkbox value
valueCopy[index] = e.target.checked ? e.target.value : null
// send data to react hook form
field.onChange(valueCopy)
// update local state
setValue(valueCopy)
}}
key={option}
checked={value.includes(option)}
type="checkbox"
value={option}
/>
))}
</>
)
}
export default function App() {
const { register, handleSubmit, control } = useForm({
defaultValues: {
controlled: [],
uncontrolled: [],
},
})
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<section>
<h2>uncontrolled</h2>
<input {...register("uncontrolled")} type="checkbox" value="A" />
<input {...register("uncontrolled")} type="checkbox" value="B" />
<input {...register("uncontrolled")} type="checkbox" value="C" />
</section>
<section>
<h2>controlled</h2>
<Checkboxes
options={["a", "b", "c"]}
control={control}
name="controlled"
/>
</section>
<input type="submit" />
</form>
)
}
팁
-
MUI, AntD, Chakra UI 같은 외부 제어 컴포넌트를 사용할 때는 각 속성의 역할을 이해해야 합니다. 이 훅은 입력을 관찰하고, 상태를 보고하며, 값을 설정합니다.
- onChange: 데이터를 React Hook Form으로 다시 전달합니다.
- onBlur: 입력과 상호작용했음을 보고합니다(포커스 및 blur).
- value: 입력의 초기값과 갱신된 값을 설정합니다.
- ref: 오류가 있는 입력에 포커스를 설정할 수 있게 합니다. 대상 컴포넌트가 ref를 전달하거나(
React.forwardRef) MUI의inputRef처럼 동등한 기능을 노출할 때만 작동합니다. ref를 전혀 받지 않는다면 spread 대상에서ref를 제외하고 포커스를 직접 처리합니다. - name: 입력에 고유한 이름을 부여합니다.
자체 상태를 유지하면서
useController와 함께 사용해도 됩니다.const { field } = useController({ name: 'test' });
const [value, setValue] = useState(field.value);
onChange={(event) => {
field.onChange(parseInt(event.target.value)) // data sent back to hook form
setValue(event.target.value) // UI state
}} -
입력을 다시
register하지 않습니다. 이 사용자 정의 훅이 등록 과정을 처리하도록 설계되어 있습니다.const { field } = useController({ name: 'test' })
<input {...field} /> // ✅
<input {...field} {...register('test')} /> // ❌ double up the registration -
컴포넌트마다
useController를 한 번만 호출하는 것이 좋습니다. 호출할 때마다 별도의 구독이 생성되므로 한 컴포넌트에서 여러 번 호출하면 불필요한 리렌더링이 발생할 수 있습니다. 같은 컴포넌트에 제어 필드가 둘 이상 필요하다면 구조 분해한 각field의 이름을 바꿔 이름 충돌을 피하거나Controller사용을 고려합니다.const { field: input } = useController({ name: 'test' })
const { field: checkbox } = useController({ name: 'test1' })
<input {...input} />
<input {...checkbox} />