Form values
Form values (폼 값)
useForm에서 폼 값을 초기화, 설정, 변환, 감시하는 다양한 방법을 다뤄요. form.initialize, setFieldValue, reset, transformValues, watch, useWatchValue 등 값을 다루는 API를 하나씩 설명해 드릴게요.
출처: 문서
본문
초기화 (form.initialize)
TanStack Query (react-query)와 함께 사용하는 예시예요:
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useForm } from '@mantine/form';
function Demo() {
const query = useQuery({
queryKey: ['current-user'],
queryFn: () => fetch('/api/users/me').then((res) => res.json()),
});
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
email: '',
},
});
useEffect(() => {
if (query.data) {
// Even if query.data changes, the form will be initialized only once
form.initialize(query.data);
}
}, [query.data]);
}
form.initialize는 호출되기 전에 설정된 모든 값을 지워요. 데이터 손실을 막으려면 form.initialize가 호출되기 전에 모든 폼 필드에 readOnly나 disabled를 설정하는 것이 좋아요. 이를 enhanceGetInputProps로 구현할 수 있어요:
import { NumberInput, TextInput, Button } from '@mantine/core';
import { useForm } from '@mantine/form';
interface FormValues {
name: string;
age: number | string;
}
function Demo() {
const form = useForm<FormValues>({
mode: 'uncontrolled',
initialValues: { name: '', age: '' },
enhanceGetInputProps: (payload) => {
if (!payload.form.initialized) {
return { disabled: true };
}
return {};
},
});
return (
<>
<TextInput
{...form.getInputProps('name')}
key={form.key('name')}
label="Your name"
placeholder="Your name"
/>
<NumberInput
{...form.getInputProps('age')}
key={form.key('age')}
label="Age"
placeholder="Age"
mt="md"
/>
<Button onClick={() => form.initialize({ name: 'John', age: 20 })} mt="md">
Initialize form
</Button>
</>
);
}
setFieldValue 핸들러
form.setFieldValue 핸들러는 주어진 경로의 필드 값을 설정할 수 있게 해줘요:
import { useForm } from '@mantine/form';
import { TextInput, Button, Group } from '@mantine/core';
import { randomId } from '@mantine/hooks';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
email: '',
},
});
return (
<div>
<TextInput
label="Name"
placeholder="Name"
key={form.key('name')}
{...form.getInputProps('name')}
/>
<TextInput
mt="md"
label="Email"
placeholder="Email"
key={form.key('email')}
{...form.getInputProps('email')}
/>
<Group justify="center" mt="xl">
<Button onClick={() => form.setFieldValue('name', randomId())}>Random name</Button>
<Button onClick={() => form.setFieldValue('email', `${randomId()}@test.com`)}>
Random email
</Button>
</Group>
</div>
);
}
reset 핸들러
form.reset 핸들러는 값을 initialValues로 설정하고 모든 오류를 지워요:
import { useForm } from '@mantine/form';
import { TextInput, Button, Group } from '@mantine/core';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
email: '',
},
});
return (
<div>
<TextInput
label="Name"
placeholder="Name"
key={form.key('name')}
{...form.getInputProps('name')}
/>
<TextInput
mt="md"
label="Email"
placeholder="Email"
key={form.key('email')}
{...form.getInputProps('email')}
/>
<Group justify="center" mt="xl">
<Button onClick={() => form.reset()}>Reset to initial values</Button>
</Group>
</div>
);
}
setInitialValues 핸들러
form.setInitialValues 핸들러는 폼이 초기화된 후에 initialValues를 갱신하게 해줘요:
import { useEffect } from 'react';
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
email: '',
},
});
useEffect(() => {
fetch('/api/user')
.then((res) => res.json())
.then((data) => {
// Update initial values after the form was initialized
// These values will be used in form.reset
// and to compare values to get the dirty state
form.setInitialValues(data);
form.setValues(data);
});
}, []);
}
transformValues
transformValues를 사용해 onSubmit 핸들러에서 제출되기 전에 값을 변환해요. 예를 들어 여러 필드를 하나로 합치거나 타입을 변환할 때 사용할 수 있어요:
import { useState } from 'react';
import { useForm } from '@mantine/form';
import { TextInput, Button, Code } from '@mantine/core';
function Demo() {
const [submittedValues, setSubmittedValues] = useState('');
const form = useForm({
mode: 'uncontrolled',
initialValues: {
firstName: 'Jane',
lastName: 'Doe',
age: '33',
},
transformValues: (values) => ({
fullName: `${values.firstName} ${values.lastName}`,
age: Number(values.age) || 0,
}),
});
return (
<>
<form
onSubmit={form.onSubmit((values) => setSubmittedValues(JSON.stringify(values, null, 2)))}
>
<TextInput
label="First name"
placeholder="First name"
key={form.key('firstName')}
{...form.getInputProps('firstName')}
/>
<TextInput
label="Last name"
placeholder="Last name"
mt="md"
key={form.key('lastName')}
{...form.getInputProps('lastName')}
/>
<TextInput
type="number"
label="Age"
placeholder="Age"
mt="md"
key={form.key('age')}
{...form.getInputProps('age')}
/>
<Button type="submit" mt="md">
Submit
</Button>
</form>
{submittedValues && (
<Code block mt="md">
{submittedValues}
</Code>
)}
</>
);
}
변환된 값 가져오기
form.getTransformedValues를 호출하면 form.onSubmit 메서드 밖에서도 변환된 값을 얻을 수 있어요. 변환할 values를 선택적 인자로 받아요. 제공하지 않으면 form.getValues() 변환 결과를 대신 반환해요:
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
firstName: 'John',
lastName: 'Doe',
},
transformValues: (values) => ({
fullName: `${values.firstName} ${values.lastName}`,
}),
});
form.getTransformedValues(); // -> { fullName: 'John Doe' }
form.getTransformedValues({
firstName: 'Jane',
lastName: 'Loe',
}); // { fullName: 'Jane Loe' }
}
onValuesChange
onValuesChange 함수는 폼 값이 바뀔 때마다 호출돼요. 폼 값 변경을 구독할 때 useEffect 대신 이 함수를 사용해요:
import { TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
email: '',
},
onValuesChange: (values) => {
console.log(values);
},
});
return (
<div>
<TextInput
label="Name"
placeholder="Name"
key={form.key('name')}
{...form.getInputProps('name')}
/>
<TextInput
mt="md"
label="Email"
placeholder="Email"
key={form.key('email')}
{...form.getInputProps('email')}
/>
</div>
);
}
form.watch
form.watch는 특정 폼 필드의 변경을 구독할 수 있는 이펙트 함수예요. 필드 경로와 콜백 함수를 받으며, 콜백은 새 값, 이전 값, touched와 dirty 필드 상태와 함께 호출돼요:
import { TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
email: '',
},
});
form.watch('name', ({ previousValue, value, touched, dirty }) => {
console.log({ previousValue, value, touched, dirty });
});
return (
<div>
<TextInput label="Name" placeholder="Name" {...form.getInputProps('name')} />
<TextInput mt="md" label="Email" placeholder="Email" {...form.getInputProps('email')} />
</div>
);
}
form.watch는 내부적으로 useEffect를 사용한다는 점에 주의하세요. 모든 훅 규칙이 적용돼요. 예를 들어 form.watch를 조건부로 쓰거나 루프 안에서 사용할 수 없어요:
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: '' },
});
// ❌ This will not work
if (Math.random() > 0.5) {
form.watch('name', ({ previousValue, value, touched, dirty }) => {
console.log({ previousValue, value, touched, dirty });
});
}
}
form.useWatchValue
form.useWatchValue는 단일 필드에 구독하고 현재 값을 반환해요. 콜백을 받는 form.watch와 달리 값을 직접 반환하고, 해당 필드가 변경되면 호출한 컴포넌트를 다시 렌더링해요. 렌더링 중에 폼 값이 필요할 때 — 예를 들어 폼 일부를 조건부로 표시할 때 — 사용해요:
import { Checkbox, Collapse, Stack, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
email: '',
shipsInternationally: false,
country: '',
},
});
const shipsInternationally = form.useWatchValue('shipsInternationally');
return (
<Stack>
<TextInput
label="Email"
placeholder="Email"
key={form.key('email')}
{...form.getInputProps('email')}
/>
<Checkbox
label="Ships internationally"
key={form.key('shipsInternationally')}
{...form.getInputProps('shipsInternationally', { type: 'checkbox' })}
/>
<Collapse expanded={shipsInternationally}>
<TextInput
label="Country"
placeholder="Country"
key={form.key('country')}
{...form.getInputProps('country')}
/>
</Collapse>
</Stack>
);
}
form.useWatchValue는 제어/비제어 두 모드에서 동일하게 동작해요. 비제어 모드에서 렌더링 중 form.getValues()로 값을 읽으면 값이 변경돼도 컴포넌트가 다시 렌더링되지 않아요. 이럴 때 form.useWatchValue를 사용하세요:
import { Collapse } from '@mantine/core';
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { shipsInternationally: false },
});
// ❌ Does not rerender the component when the value changes
const staleValue = form.getValues().shipsInternationally;
// ✅ Rerenders the component when the value changes
const shipsInternationally = form.useWatchValue('shipsInternationally');
return <Collapse expanded={shipsInternationally}>{/* ... */}</Collapse>;
}
form.useWatchValue를 호출한 컴포넌트만 다시 렌더링되고, 주어진 필드의 값이 변경될 때에만 그렇게 돼요. 다른 필드의 변경은 다시 렌더링을 유발하지 않아요.
form.useWatchValue는 훅이므로 모든 훅 규칙이 적용돼요. 조건부로 호출하거나 루프 안에서 사용할 수 없어요:
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: '' },
});
// ❌ This will not work
if (Math.random() > 0.5) {
const name = form.useWatchValue('name');
}
}
배열이 있는 form.watch
form.watch는 배열 필드에서도 동작해요. 배열 안의 어떤 중첩 필드가 변경되거나 목록 연산(insertListItem, removeListItem, reorderListItem, replaceListItem)이 수행되면 콜백이 실행돼요:
import { useState } from 'react';
import { Button, Code, Group, NumberInput, Stack, Text, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { randomId } from '@mantine/hooks';
function Demo() {
const [total, setTotal] = useState(0);
const form = useForm({
mode: 'uncontrolled',
initialValues: {
products: [
{ name: 'Apple', price: 2, quantity: 3, key: randomId() },
{ name: 'Orange', price: 1, quantity: 5, key: randomId() },
],
},
});
form.watch('products', ({ value }) => {
setTotal(value.reduce((acc, item) => acc + item.price * item.quantity, 0));
});
return (
<Stack>
<Text fw={700}>Total: ${total}</Text>
{form.getValues().products.map((item, index) => (
<Group key={item.key} align="flex-end">
<TextInput
label="Name"
style={{ flex: 1 }}
key={form.key(`products.${index}.name`)}
{...form.getInputProps(`products.${index}.name`)}
/>
<NumberInput
label="Price"
style={{ width: 80 }}
key={form.key(`products.${index}.price`)}
{...form.getInputProps(`products.${index}.price`)}
/>
<NumberInput
label="Qty"
style={{ width: 80 }}
key={form.key(`products.${index}.quantity`)}
{...form.getInputProps(`products.${index}.quantity`)}
/>
<Button
color="red"
onClick={() => form.removeListItem('products', index)}
>
Remove
</Button>
</Group>
))}
<Group>
<Button
onClick={() =>
form.insertListItem('products', {
name: '',
price: 0,
quantity: 1,
key: randomId(),
})
}
>
Add product
</Button>
</Group>
<Code block>{JSON.stringify(form.getValues(), null, 2)}</Code>
</Stack>
);
}
form.watch 캐스케이드 (cascade)
기본적으로 form.watch는 중첩 필드가 변경되면 부모 watcher에 알려요(상향 캐스케이드). 하향 캐스케이드(부모가 직접 설정될 때 중첩 필드 watcher에도 알림)를 활성화하려면 cascadeUpdates: true로 설정해요:
import { Button, Code, Stack, TextInput } from '@mantine/core';
import { createFormContext } from '@mantine/form';
import { useState } from 'react';
const [Provider, usePersonFormContext, usePersonForm] = createFormContext<{ person: { name: string } }>();
function Demo() {
const form = usePersonForm({
mode: 'uncontrolled',
cascadeUpdates: true,
initialValues: {
person: { name: "" }
}
})
return (
<Provider form={form}>
<Stack>
<TextInput
label="Name"
placeholder="Name"
key={form.key('person.name')}
{...form.getInputProps('person.name')}
/>
<Button onClick={() => form.setFieldValue("person", { name: "Jane Doe" })}>Set 'person' object to `{'{ name: "Jane Doe" }'}`</Button>
<Watcher />
</Stack>
</Provider>
);
}
function Watcher() {
const form = usePersonFormContext();
const [person, setPerson] = useState<{ name: string }>();
const [name, setName] = useState<string>();
form.watch('person', ({ value }) => setPerson(value));
form.watch("person.name", ({ value }) => setName(value));
return <Code block>{JSON.stringify({ person, name }, null, 2)}</Code>
}
값 타입 가져오기
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({ initialValues: { name: '', age: 0 } });
// Get the inferred form values type, will be `{ name: string; age: number }`
type FormValues = typeof form.values;
// Use the values type in handleSubmit function or anywhere else
const handleSubmit = (values: FormValues) => console.log(values);
}
변환된 값 타입 가져오기
변환된 값(transformValues의 출력)을 얻으려면 TransformedValues 타입을 사용해요. 커스텀 제출 함수를 만들 때 유용해요:
import { TransformedValues, useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
locationId: '2',
},
transformValues: (values) => ({
...values,
locationId: Number(values.locationId),
}),
});
type Transformed = TransformedValues<typeof form>;
// -> { name: string, locationId: number }
const handleSubmit = (values: TransformedValues<typeof form>) => {};
return <form onSubmit={form.onSubmit(handleSubmit)} />;
}
값 타입 설정하기
기본적으로 폼 값 타입은 initialValues에서 추론돼요. 이를 피하려면 useForm 훅에 타입을 전달할 수 있어요. 타입을 올바르게 추론할 수 없거나 더 구체적인 타입을 제공하려 할 때 유용해요:
import { useForm } from '@mantine/form';
interface FormValues {
name: string; // regular field, same as inferred type
role: 'user' | 'admin'; // union, more specific than inferred string type
// values that may be undefined or null
// cannot be correctly inferred in strict mode
age: number | undefined;
registeredAt: Date | null;
// Arrays that are empty cannot be inferred correctly
jobs: string[];
}
function Demo() {
const form = useForm<FormValues>({
mode: 'uncontrolled',
initialValues: {
name: '',
role: 'user',
age: undefined,
registeredAt: null,
jobs: [],
},
});
}
변환된 값 타입 설정하기
기본적으로 변환된 값 타입은 폼 값 타입과 같아요. 다른 타입을 설정하려면 useForm에 두 번째 제네릭 인자를 전달할 수 있어요:
import { useForm } from '@mantine/form';
interface FormValues {
name: string;
locationId: string;
}
interface TransformedValues {
name: string;
locationId: number;
}
function Demo() {
const form = useForm<FormValues, TransformedValues>({
mode: 'uncontrolled',
initialValues: {
name: '',
locationId: '2',
},
transformValues: (values) => ({
...values,
locationId: Number(values.locationId),
}),
});
}
더 알아보기 (Learn more)
- Uncontrolled mode — 비제어 모드
- getInputProps — getInputProps