본문으로 건너뛰기

useFieldArray

</> useFieldArray: UseFieldArrayProps

필드 배열(동적 폼)을 다루는 커스텀 훅입니다. 더 나은 사용자 경험과 성능을 제공하기 위해 만들어졌습니다. 이 짧은 동영상에서 성능 향상을 확인할 수 있습니다.

Props


이름타입필수설명
namestring필드 배열의 이름입니다. 참고: 동적 이름은 지원하지 않습니다.
controlObjectcontroluseForm에서 제공하는 객체입니다. FormProvider를 사용한다면 선택 사항입니다.
shouldUnregisterboolean언마운트 후 필드 배열의 등록을 해제할지 여부입니다.
disabledbooleanv7.79.0부터 전체 필드 배열을 비활성화합니다. true이면 fields는 그대로 채워져 있지만 각 항목의 disabled 속성이 true로 설정되고(반환값 참고), 모든 변경 메서드(append, prepend, insert, remove, swap, move, update, replace)가 아무 작업도 하지 않으며 배열 내부 구독도 설정되지 않습니다. 판별 유니언 폼 구조에서 필드 배열을 조건부로 활성화할 때 유용합니다.
keyNamestring = "id"key prop으로 사용할 자동 생성 식별자가 들어 있는 속성 이름입니다. 필드 객체에 이미 id 속성이 있다면(예: 데이터베이스 레코드) 사용자 정의 keyName을 설정하지 않는 한 자동 생성 식별자가 기존 값을 덮어씁니다. 이 prop은 더 이상 필요하지 않으며 다음 메이저 버전에서 제거됩니다.
rulesObjectv7.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 })[]각 항목에 컴포넌트의 각 행에 대한 defaultValuekey가 들어 있는 배열입니다. 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) => voidv7.11.0부터 특정 위치의 입력을 하나 이상 업데이트합니다. 업데이트된 필드는 언마운트 후 다시 마운트됩니다. 원하는 동작이 아니라면 setValue API를 대신 사용합니다. 중요: update 데이터는 필수이며 일부만 제공할 수 없습니다.
replace(obj: object[]) => voidv7.15.0부터 전체 필드 배열의 값을 교체합니다.
remove(index?: number | number[]) => void특정 위치의 입력을 하나 이상 제거합니다. 인덱스를 제공하지 않으면 모두 제거합니다.

규칙


  • useFieldArrayid라는 고유 식별자를 자동으로 생성하며 key prop에 사용합니다. 이것이 필요한 이유는 목록 렌더링을 참고하세요.

    리렌더링으로 필드가 깨지지 않게 하려면 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를 활성화하면 새로 추가한 필드가 리렌더링 시 등록 해제되어 값이 사라집니다. useFieldArrayshouldUnregister: true와 함께 사용하지 마세요.

  • 항목별 disabled 옵션은 없습니다. 각 객체의 disabled 플래그는 fields에 있으며(반환값 참고), 훅 수준의 disabled prop을 그대로 반영하여 모든 항목에 동일하게 적용됩니다. 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>
)
}

동영상



사용자 정의 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>
)
}