useFieldArray
</> useFieldArray: UseFieldArrayProps
필드 배열(동적 폼)을 다루는 커스텀 훅입니다. 더 나은 사용자 경험과 성능을 제공하기 위해 만들어졌습니다. 이 짧은 동영상에서 성능 향상을 확인할 수 있습니다.
Props
| 이름 | 타입 | 필수 | 설명 |
|---|---|---|---|
name | string | ✓ | 필드 배열의 이름입니다. 참고: 동적 이름은 지원하지 않습니다. |
control | Object | control은 useForm에서 제공하는 객체입니다. FormProvider를 사용한다면 선택 사항입니다. | |
shouldUnregister | boolean | 언마운트 후 필드 배열의 등록을 해제할지 여부입니다. | |
disabled | boolean | v7.79.0부터 전체 필드 배열을 비활성화합니다. true이면 fields는 그대로 채워져 있지만 각 항목의 disabled 속성이 true로 설정되고(반환값 참고), 모든 변경 메서드(append, prepend, insert, remove, swap, move, update, replace)가 아무 작업도 하지 않으며 배열 내부 구독도 설정되지 않습니다. 판별 유니언 폼 구조에서 필드 배열을 조건부로 활성화할 때 유용합니다. | |
keyName | string = "id" | key prop으로 사용할 자동 생성 식별자가 들어 있는 속성 이름입니다. 필드 객체에 이미 id 속성이 있다면(예: 데이터베이스 레코드) 사용자 정의 keyName을 설정하지 않는 한 자동 생성 식별자가 기존 값을 덮어씁니다. 이 prop은 더 이상 필요하지 않으며 다음 메이저 버전에서 제거됩니다. | |
rules | Object | v7.34.0부터 검증 rules API는 register와 같으며 required, minLength, maxLength, validate를 포함합니다. 검증 오류가 발생하면 root 속성이 formState.errors?.<name>?.root에 추가됩니다(예: formState.errors?.test?.root는 이름이 test인 필드 배열). 타입은 FieldError입니다. 중요: 내장 검증에만 적용됩니다. |
Props 예제
function FieldArray() {
const { control, register } = useForm()
const { fields, append, prepend, remove, swap, move, insert } = useFieldArray(
{
control, // control props comes from useForm (optional: if you are using FormProvider)
name: "test", // unique name for your Field Array
}
)
return (
<>
{fields.map((field, index) => (
<input
key={field.id} // important to include key with field's id
{...register(`test.${index}.value`)}
/>
))}
</>
)
}
반환값
| 이름 | 타입 | 설명 |
|---|---|---|
fields | (object & { id: string })[] | 각 항목에 컴포넌트의 각 행에 대한 defaultValue와 key가 들어 있는 배열입니다. v7.80.0부터 훅 수준의 disabled prop을 설정하면 각 항목의 disabled 속성에 반영됩니다. 다만 등록된 입력에 자동으로 전달되지는 않으므로 입력에 직접 spread해야 합니다(규칙 참고). |
append | (obj: object | object[], focusOptions) => void | 필드 끝에 입력을 하나 이상 추가하고 포커스합니다. 이 동작 중에 입력 값이 등록됩니다. 중요: append 데이터는 필수이며 일부만 제공할 수 없습니다. v7.0.0부터 focusOptions는 { shouldFocus?: boolean = true, focusIndex?: number, focusName?: string }을 받습니다. |
prepend | (obj: object | object[], focusOptions) => void | 필드 시작 부분에 입력을 하나 이상 추가하고 포커스합니다. 이 동작 중에 입력 값이 등록됩니다. 중요: prepend 데이터는 필수이며 일부만 제공할 수 없습니다. v7.0.0부터 focusOptions는 { shouldFocus?: boolean = true, focusIndex?: number, focusName?: string }을 받습니다. |
insert | (index: number, value: object | object[], focusOptions) => void | 특정 위치에 입력을 하나 이상 삽입하고 포커스합니다. 중요: insert 데이터는 필수이며 일부만 제공할 수 없습니다. v7.0.0부터 focusOptions는 { shouldFocus?: boolean = true, focusIndex?: number, focusName?: string }을 받습니다. |
swap | (from: number, to: number) => void | 입력의 위치를 서로 바꿉니다. |
move | (from: number, to: number) => void | 입력을 하나 이상 다른 위치로 이동합니다. |
update | (index: number, obj: object) => void | v7.11.0부터 특정 위치의 입력을 하나 이상 업데이트합니다. 업데이트된 필드는 언마운트 후 다시 마운트됩니다. 원하는 동작이 아니라면 setValue API를 대신 사용합니다. 중요: update 데이터는 필수이며 일부만 제공할 수 없습니다. |
replace | (obj: object[]) => void | v7.15.0부터 전체 필드 배열의 값을 교체합니다. |
remove | (index?: number | number[]) => void | 특정 위치의 입력을 하나 이상 제거합니다. 인덱스를 제공하지 않으면 모두 제거합니다. |
규칙
-
useFieldArray는id라는 고유 식별자를 자동으로 생성하며keyprop에 사용합니다. 이것이 필요한 이유는 목록 렌더링을 참고하세요.리렌더링으로 필드가 깨지지 않게 하려면
field.id를 컴포넌트 키로 추가해야 하며index를 사용하면 안 됩니다.// ✅ correct:
{fields.map((field, index) => <input key={field.id} ... />)}
// ❌ incorrect:
{fields.map((field, index) => <input key={index} ... />)} -
작업을 연달아 쌓지 않는 것이 좋습니다.
// ❌ avoid stacking actions in the same handler
onClick={() => {
append({ test: 'test' });
remove(0);
}}
// ✅ Better solution: the remove action happens after the second render
useEffect(() => {
remove(0);
}, [remove])
onClick={() => {
append({ test: 'test' });
}} -
각
useFieldArray는 고유하며 자체 상태 업데이트를 가집니다. 따라서useFieldArray인스턴스 여러 개에 동일한name을 사용하면 안 됩니다. -
각 입력 이름은 고유해야 합니다. 같은 이름의 체크박스나 라디오 버튼을 만들어야 한다면
useController또는Controller와 함께 사용합니다. -
평면 필드 배열은 지원하지 않습니다. 필드 배열의 각 항목은 원시 값이 아니라 객체여야 합니다.
{ test: [{ value: 'a' }] }✅,{ test: ['a', 'b'] }❌. -
shouldUnregister: true는 지원하지 않습니다. 필드 배열은 내부 상태를 관리하기 위해 입력의 마운트와 언마운트에 의존합니다.shouldUnregister를 활성화하면 새로 추가한 필드가 리렌더링 시 등록 해제되어 값이 사라집니다.useFieldArray를shouldUnregister: true와 함께 사용하지 마세요. -
항목별
disabled옵션은 없습니다. 각 객체의disabled플래그는fields에 있으며(반환값 참고), 훅 수준의disabledprop을 그대로 반영하여 모든 항목에 동일하게 적용됩니다.append/prepend/insert에 전달한 데이터에서 읽지 않으며 등록된 입력으로 자동 전달되지도 않습니다. 직접 연결해야 합니다.const { fields, append } = useFieldArray({
control,
name: "test",
disabled: true,
})
{
fields.map((field, index) => (
<input
key={field.id}
disabled={field.disabled}
{...register(`test.${index}.value`)}
/>
))
} -
필드 배열을 append, prepend, insert, update할 때 객체는 빈 객체
{}일 수 없습니다. 모든 입력의defaultValues를 제공해야 합니다.append() // ❌
append({}) // ❌
append({ firstName: "bill", lastName: "luo" }) // ✅
TypeScript
-
입력
name을 등록할 때const로 캐스팅해야 합니다.<input key={field.id} {...register(`test.${index}.test` as const)} /> -
순환 참조는 지원하지 않습니다. 자세한 내용은 이 GitHub 이슈를 참고하세요.
-
중첩 필드 배열에서는 필드 배열을 이름으로 캐스팅해야 합니다.
const { fields } = useFieldArray({ name: `test.${index}.keyValue` as 'test.0.keyValue' });
예제
import { useForm, useFieldArray } from "react-hook-form"
function App() {
const { register, control, handleSubmit, reset, trigger, setError } = useForm(
{
// defaultValues: {}; you can populate the fields by this attribute
}
)
const { fields, append, remove } = useFieldArray({
control,
name: "test",
})
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<ul>
{fields.map((item, index) => (
<li key={item.id}>
<input {...register(`test.${index}.firstName`)} />
<Controller
render={({ field }) => <input {...field} />}
name={`test.${index}.lastName`}
control={control}
/>
<button type="button" onClick={() => remove(index)}>
Delete
</button>
</li>
))}
</ul>
<button
type="button"
onClick={() => append({ firstName: "bill", lastName: "luo" })}
>
append
</button>
<input type="submit" />
</form>
)
}
import { useForm, useFieldArray, useWatch, Control } from "react-hook-form"
type FormValues = {
cart: {
name: string
price: number
quantity: number
}[]
}
const Total = ({ control }: { control: Control<FormValues> }) => {
const formValues = useWatch({
name: "cart",
control,
})
const total = formValues.reduce(
(acc, current) => acc + (current.price || 0) * (current.quantity || 0),
0
)
return <p>Total Amount: {total}</p>
}
export default function App() {
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
defaultValues: {
cart: [{ name: "test", quantity: 1, price: 23 }],
},
mode: "onBlur",
})
const { fields, append, remove } = useFieldArray({
name: "cart",
control,
})
const onSubmit = (data: FormValues) => console.log(data)
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => {
return (
<div key={field.id}>
<section className={"section"} key={field.id}>
<input
placeholder="name"
{...register(`cart.${index}.name` as const, {
required: true,
})}
className={errors?.cart?.[index]?.name ? "error" : ""}
/>
<input
placeholder="quantity"
type="number"
{...register(`cart.${index}.quantity` as const, {
valueAsNumber: true,
required: true,
})}
className={errors?.cart?.[index]?.quantity ? "error" : ""}
/>
<input
placeholder="value"
type="number"
{...register(`cart.${index}.price` as const, {
valueAsNumber: true,
required: true,
})}
className={errors?.cart?.[index]?.price ? "error" : ""}
/>
<button type="button" onClick={() => remove(index)}>
DELETE
</button>
</section>
</div>
)
})}
<Total control={control} />
<button
type="button"
onClick={() =>
append({
name: "",
quantity: 0,
price: 0,
})
}
>
APPEND
</button>
<input type="submit" />
</form>
</div>
)
}
import { useForm, useFieldArray, useWatch } from "react-hook-form"
export default function App() {
const { control, handleSubmit } = useForm()
const { fields, append, update } = useFieldArray({
control,
name: "array",
})
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{fields.map((field, index) => (
<Edit
key={field.id}
control={control}
update={update}
index={index}
value={field}
/>
))}
<button
type="button"
onClick={() => {
append({ firstName: "" })
}}
>
append
</button>
<input type="submit" />
</form>
)
}
const Display = ({ control, index }) => {
const data = useWatch({
control,
name: `array.${index}`,
})
return <p>{data?.firstName}</p>
}
const Edit = ({ update, index, value, control }) => {
const { register, handleSubmit } = useForm({
defaultValues: value,
})
return (
<div>
<Display control={control} index={index} />
<input
placeholder="first name"
{...register(`firstName`, { required: true })}
/>
<button
type="button"
onClick={handleSubmit((data) => update(index, data))}
>
Submit
</button>
</div>
)
}
import { useForm, useWatch, useFieldArray, Control } from "react-hook-form"
const ConditionField = ({ control, index, register }) => {
const output = useWatch({
name: "data",
control,
defaultValue: "yay! I am watching you :)",
})
return (
<>
{output[index]?.name === "bill" && (
<input {...register(`data[${index}].conditional`)} />
)}
<input
{...register(`data[${index}].easyConditional`)}
style={{ display: output[index]?.name === "bill" ? "block" : "none" }}
/>
</>
)
}
const UseFieldArrayUnregister = () => {
const { control, handleSubmit, register } = useForm({
defaultValues: {
data: [{ name: "test" }, { name: "test1" }, { name: "test2" }],
},
mode: "onSubmit",
shouldUnregister: false,
})
const { fields } = useFieldArray({
control,
name: "data",
})
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((data, index) => (
<>
<input {...register(`data[${index}].name`)} />
<ConditionField control={control} register={register} index={index} />
</>
))}
<input type="submit" />
</form>
)
}
import { useForm, useFieldArray } from "react-hook-form"
const App = () => {
const { register, control } = useForm({
defaultValues: {
test: [{ value: "1" }, { value: "2" }],
},
})
const { fields, prepend, append } = useFieldArray({
name: "test",
control,
})
return (
<form>
{fields.map((field, i) => (
<input key={field.id} {...register(`test.${i}.value` as const)} />
))}
<button
type="button"
onClick={() => prepend({ value: "" }, { focusIndex: 1 })}
>
prepend
</button>
<button
type="button"
onClick={() => append({ value: "" }, { focusName: "test.0.value" })}
>
append
</button>
</form>
)
}
동영상
팁
사용자 정의 register
입력을 register할 때 실제 입력 없이 Controller에서 처리할 수도 있습니다. 복잡한 데이터 구조를 사용하거나 실제 데이터가 입력 안에 저장되지 않을 때 useFieldArray를 빠르고 유연하게 사용할 수 있습니다.
import { useForm, useFieldArray, Controller, useWatch } from "react-hook-form"
const ConditionalInput = ({ control, index, field }) => {
const value = useWatch({
name: "test",
control,
})
return (
<Controller
control={control}
name={`test.${index}.firstName`}
render={({ field }) =>
value?.[index]?.checkbox === "on" ? <input {...field} /> : null
}
/>
)
}
function App() {
const { control, register } = useForm()
const { fields, append, prepend } = useFieldArray({
control,
name: "test",
})
return (
<form>
{fields.map((field, index) => (
<ConditionalInput key={field.id} {...{ control, index, field }} />
))}
</form>
)
}
제어 필드 배열
전체 필드 배열을 제어하여 각 onChange가 fields 객체에 반영되게 해야 하는 경우가 있습니다.
import { useForm, useFieldArray } from "react-hook-form"
export default function App() {
const { register, handleSubmit, control, watch } = useForm()
const { fields, append } = useFieldArray({
control,
name: "fieldArray",
})
const watchFieldArray = watch("fieldArray")
const controlledFields = fields.map((field, index) => {
return {
...field,
...watchFieldArray[index],
}
})
return (
<form>
{controlledFields.map((field, index) => {
return (
<input
key={field.id}
{...register(`fieldArray.${index}.name` as const)}
/>
)
})}
</form>
)
}