Skip to content

useFieldArray

필드 배열을 위한 React 훅

useFieldArray: UseFieldArrayProps

필드 배열(동적 폼)을 다루는 커스텀 훅으로, 더 나은 사용자 경험과 성능을 제공하는 데 초점을 맞추고 있습니다. 짧은 영상 에서 성능 향상의 차이를 확인할 수 있습니다.

Props

NameTypeRequiredDescription
namestring

필드 배열의 이름. 참고: 동적인 이름은 지원하지 않습니다.

controlObjectuseForm에서 제공하는 control 객체. FormProvider를 사용하고 있다면 선택 사항입니다.
shouldUnregisterboolean

필드 배열이 언마운트된 후, 등록 해제될 지 여부.

keyNamestring = id

자동 생성된 식별자를 key prop으로 사용하기 위한 속성의 이름. 이 prop은 더 이상 필수 항목이 아니며, 다음 주요 버전에서 제거될 예정입니다.

rulesObject

register와 동일한 유효성 검사 rules API를 사용하며, 포함되는 규칙은 다음과 같습니다:

required, minLength, maxLength, validate

useFieldArray({
rules: { minLength: 4 }
})

만약 유효성 검사 에러가 발생하면, root 속성이 formState.errors?.fieldArray?.root에 추가되며, 이는 FieldError 타입을 가집니다.

중요: 이 동작은내장된 검증에만 적용됩니다.

Examples

function FieldArray() {
const { control, register } = useForm();
const { fields, append, prepend, remove, swap, move, insert } = useFieldArray({
control, // control props는 useForm에서 제공됨 (FormProvider를 사용 중이라면 선택 사항)
name: "test", // 필드 배열의 고유한 이름
});
return (
{fields.map((field, index) => (
<input
key={field.id} // 필드의 id를 key로 포함하는 것이 중요함
{...register(`test.${index}.value`)}
/>
))}
);
}

Return

NameTypeDescription
fieldsobject & { id: string }object에는 컴포넌트의 defaultValue key가 포함됩니다.
append(obj: object | object[], focusOptions) => void

입력 필드를 기존 필드의 끝에 추가하고 포커스를 이동합니다. 이 과정에서 입력 값이 등록(registered)됩니다

중요: 추가할 데이터는 필수이며, 일부만 제공될 수 없습니다.

prepend(obj: object | object[], focusOptions) => void

입력 필드를 기존 필드의 시작 부분에 추가하고 포커스를 이동합니다. 이 과정에서 입력 값이 등록(registered)됩니다.

중요: 추가할 데이터는 필수이며, 일부만 제공될 수 없습니다.

insert(index: number, value: object | object[], focusOptions) => void

입력 필드를 특정 위치에 추가하고 포커스를 이동합니다.

중요: 추가할 데이터는 필수이며, 일부만 제공될 수 없습니다.

swap(from: number, to: number) => void입력 필드의 위치를 서로 변경합니다.
move(from: number, to: number) => void입력 필드를 다른 위치로 이동합니다.
update(index: number, obj: object) => void

입력 필드를 특정 위치에서 업데이트하면, 변경된 필드는 언마운트되었다가 다시 마운트됩니다. 이 동작을 원하지 않는 경우, setValue API를 사용하세요.

중요: 업데이트할 데이터는 필수이며, 일부만 제공될 수 없습니다.

replace(obj: object[]) => void전체 필드 배열 값을 교체합니다.
remove(index?: number | number[]) => void특정 위치의 입력 필드를 제거하거나, 인덱스를 제공하지 않으면 모든 필드를 제거합니다.

Rules

  • useFieldArraykey prop으로 사용되는 고유한 식별자인 id를 자동으로 생성합니다. 왜 이 기능이 필요한지에 대한 자세한 내용은 다음 링크를 참고하세요: https://react.dev/learn/rendering-lists

    field.id(그리고 index가 아니라)이 반드시 컴포넌트의 key로 추가되어야 합니다. 그렇지 않으면 리렌더링 시 필드가 깨질 수 있습니다:

    // ✅ correct:
    {fields.map((field, index) => <input key={field.id} ... />)}
    // ❌ incorrect:
    {fields.map((field, index) => <input key={index} ... />)}

  • 동작을 연달아 여러 번 실행하는 것은 권장되지 않습니다.

    onClick={() => {
    append({ test: 'test' });
    remove(0);
    }}
    // ✅ 더 나은 해결책: remove 동작은 두 번째 렌더링 후에 실행됩니다.
    React.useEffect(() => {
    remove(0);
    }, [remove])
    onClick={() => {
    append({ test: 'test' });
    }}
  • useFieldArray는 고유하며 자체적인 상태 업데이트를 가집니다. 즉, 동일한 name을 가진 useFieldArray를 여러 개 사용해서는 안 됩니다.

  • 각 입력 필드의 name 값은 고유해야 합니다. 만약 같은 name을 사용하는 체크박스나 라디오 버튼을 만들어야 한다면, useController 또는 Controller와 함께 사용하세요.

  • 평면 필드 배열(flat field array)은 지원되지 않습니다.

  • append, prepend, insert, update를 사용할 때, 필드 배열에 빈 객체 를 추가할 수 없습니다. 모든 입력 필드의 defaultValues를 제공해야 합니다.

    append();
    append({});
    append({ firstName: 'bill', lastName: 'luo' });

TypeScript

  • 입력 필드를 register할 때, name 값을 const로 캐스팅해야 합니다.

    <input key={field.id} {...register(`test.${index}.test` as const)} />
  • 순환 참조(circular reference)는 지원되지 않습니다. 자세한 내용은 이 Github issue 를 참고하세요.

  • 중첩된 필드 배열(nested field array)을 사용할 경우, 필드 배열을 name으로 캐스팅해야 합니다.

    const { fields } = useFieldArray({ name: `test.${index}.keyValue` as 'test.0.keyValue' });

Examples

import React from "react";
import { useForm, useFieldArray } from "react-hook-form";
function App() {
const { register, control, handleSubmit, reset, trigger, setError } = useForm({
// defaultValues: {}; 이 속성을 사용하여 필드에 값을 채울 수 있습니다.
});
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 * as React from "react";
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 React from 'react';
import { useForm, useWatch, useFieldArray, Control } from 'react-hook-form';
type FormValues = {
data: { name: string }[];
};
const ConditionField = ({
control,
index,
register,
}: {
control: Control<FormValues>;
index: number;
}) => {
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: React.FC = () => {
const { control, handleSubmit, register } = useForm<FormValues>({
defaultValues: {
data: [{ name: 'test' }, { name: 'test1' }, { name: 'test2' }],
},
mode: 'onSubmit',
shouldUnregister: false,
});
const { fields } = useFieldArray({
control,
name: 'data',
});
const onSubmit = (data: FormValues) => 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 React from 'react';
import { useForm, useFieldArray } from 'react-hook-form';
const App = () => {
const { register, control } = useForm<{
test: { value: string }[];
}>({
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>
);
};

Video

다음 영상에서 useFieldArray의 기본 사용 방법을 설명합니다.

Tips

Custom Register

실제 입력 필드가 없어도 Controller에서 입력을 register할 수 있습니다. 이를 통해 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>
);
}

Controlled Field Array

필드 배열 전체를 제어해야 하는 경우가 있을 수 있으며, 이때 각 onChange 이벤트는 fields 객체에 반영됩니다.

import { useForm, useFieldArray } from "react-hook-form";
export default function App() {
const { register, handleSubmit, control, watch } = useForm<FormValues>();
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 {...register(`fieldArray.${index}.name` as const)} />;
})}
</form>
);
}

지원해 주셔서 감사합니다

프로젝트에서 React Hook Form이 유용하다고 생각하신다면, 스타를 눌러 지원해 주시길 부탁드립니다.