Uncontrolled mode
Uncontrolled mode (비제어 모드)
7.8.0 릴리스에서 도입된 비제어(uncontrolled) 모드는 모든 폼에 권장되는 모드예요. 대형 폼에서 성능을 크게 개선해 주며, 폼 데이터를 React state 대신 ref에 저장해요. 언제 어떻게 쓰는지 하나씩 설명해 드릴게요.
출처: 문서
본문
앞선 예제에서도 볼 수 있듯 제어(controlled) 모드에서는 form.values가 매 변경마다 갱신돼요. 즉 form.values를 사용하는 모든 컴포넌트가 변경될 때마다 다시 렌더링돼요.
비제어 모드
비제어 모드는 7.8.0 릴리스에서 도입된 대체 폼 모드예요. 이제 모든 폼에 권장되는 모드이며, 대형 폼에서 상당한 성능 개선을 제공해요.
비제어 모드에서는 폼 데이터가 React state 대신 ref에 저장되고, form.values는 매 변경마다 갱신되지 않아요.
비제어 모드 폼의 예시예요:
import { useState } from 'react';
import { Button, Code, Text, TextInput } from '@mantine/core';
import { hasLength, isEmail, useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: '', email: '' },
validate: {
name: hasLength({ min: 3 }, 'Must be at least 3 characters'),
email: isEmail('Invalid email'),
},
});
const [submittedValues, setSubmittedValues] = useState<typeof form.values | null>(null);
return (
<form onSubmit={form.onSubmit(setSubmittedValues)}>
<TextInput
{...form.getInputProps('name')}
key={form.key('name')}
label="Name"
placeholder="Name"
/>
<TextInput
{...form.getInputProps('email')}
key={form.key('email')}
mt="md"
label="Email"
placeholder="Email"
/>
<Button type="submit" mt="md">
Submit
</Button>
<Text mt="md">Form values:</Text>
<Code block>{JSON.stringify(form.values, null, 2)}</Code>
<Text mt="md">Submitted values:</Text>
<Code block>{submittedValues ? JSON.stringify(submittedValues, null, 2) : '–'}</Code>
</form>
);
}
위 예제에서 볼 수 있듯 form.values는 전혀 갱신되지 않아요.
form.getValues
form.getValues 함수는 현재 폼 값을 반환해요. 컴포넌트 어디에서든 현재 폼 값을 가져올 때 사용할 수 있고, 제어/비제어 두 모드 모두에서 쓸 수 있어요.
import { useForm } from '@mantine/form';
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: 'John Doe' },
});
form.getValues(); // { name: 'John Doe' }
form.setValues({ name: 'John Smith' });
form.getValues(); // { name: 'John Smith' }
제어 모드에서도 form.values로 현재 값을 가져올 수 있지만, form.getValues를 쓰는 걸 권장해요. form.getValues는 항상 최신 값을 반환하는 반면, form.values는 비제어 모드에서 항상 오래된 값이고 제어 모드에서도 state 갱신 이전에는 오래된 값이기 때문이에요.
import { useForm } from '@mantine/form';
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: 'John Doe' },
});
const handleNameChange = () => {
form.setFieldValue('name', 'Test Name');
// ❌ Do not use form.values to get the current form values
// form.values has a stale name value until the next rerender in controlled mode
// and is always outdated in uncontrolled mode
console.log(form.values); // { name: 'John Doe' }
// ✅ Use form.getValues to get the current form values
// form.getValues always returns the latest form values
console.log(form.getValues()); // { name: 'Test Name' }
};
form.getValues()는 현재 폼 값의 ref 값을 반환해요. 즉 항상 같은 참조(reference)가 반환되므로 useEffect의 의존성 배열에 그대로 전달할 수 없어요.
import { useEffect } from 'react';
import { useForm } from '@mantine/form';
const form = useForm({ mode: 'uncontrolled' });
useEffect(() => {
// ❌ This will not work as form.getValues() is a ref value
// and will always be the same reference
}, [form.getValues()]);
useEffect로 폼 값을 관찰하는 대신 onValuesChange 콜백으로 폼 값 변경을 감지해요:
import { useForm } from '@mantine/form';
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: 'John Doe' },
onValuesChange: (values) => {
// ✅ This will be called on every form value change
console.log(values);
},
});
조건부 필드 (Conditional fields)
비제어 모드가 예상 밖으로 동작하는 가장 흔한 경우는 폼 일부를 조건부로 렌더링하는 상황이에요. form.getValues()는 현재 값을 반환하지만 컴포넌트를 변경에 구독시키지 않아요. 값을 한 번만 읽고 컴포넌트는 다시 렌더링되지 않으므로 조건이 절대 갱신되지 않아요:
import { Checkbox, Collapse, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { shipsInternationally: false, country: '' },
});
// ❌ Does not work: the component is not rerendered when the checkbox is toggled,
// the collapsed section never opens
const shipsInternationally = form.getValues().shipsInternationally;
return (
<>
<Checkbox
label="Ships internationally"
key={form.key('shipsInternationally')}
{...form.getInputProps('shipsInternationally', {
type: 'checkbox',
})}
/>
<Collapse expanded={shipsInternationally}>
<TextInput
label="Country"
key={form.key('country')}
{...form.getInputProps('country')}
/>
</Collapse>
</>
);
}
대신 form.useWatchValue를 사용해요. 단일 필드에 구독하고 그 필드가 변경될 때 컴포넌트를 다시 렌더링해요:
import { Checkbox, Collapse, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { shipsInternationally: false, country: '' },
});
// ✅ Works: the component is rerendered when the checkbox is toggled
const shipsInternationally = form.useWatchValue('shipsInternationally');
return (
<>
<Checkbox
label="Ships internationally"
key={form.key('shipsInternationally')}
{...form.getInputProps('shipsInternationally', {
type: 'checkbox',
})}
/>
<Collapse expanded={shipsInternationally}>
<TextInput
label="Country"
key={form.key('country')}
{...form.getInputProps('country')}
/>
</Collapse>
</>
);
}
form.getInputProps
form.getInputProps는 제어/비제어 모드에서 서로 다른 props를 반환해요. 제어 모드에서는 반환 객체에 value prop이 있고, 비제어 모드에서는 defaultValue prop이 있어요.
비제어 모드는 form.setFieldValue나 form.setValues가 호출될 때 컴포넌트를 갱신하기 위해 form.key()가 반환하는 key에 의존해요. 입력 컴포넌트에 반드시 form.key()가 제공하는 key를 설정해서 갱신된 값을 갖도록 해야 해요:
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { text: '' },
});
return (
<input {...form.getInputProps('text')} key={form.key('text')} />
);
}
필드 목록이 필요한 경우에는 key를 입력 컴포넌트에 직접 전달하지 마세요. 대신 래퍼 요소를 추가하고 그곳에 key를 전달해요:
import { useForm } from '@mantine/form';
import { randomId } from '@mantine/hooks';
// ❌ Incorrect: Do not override key prop, even in lists
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
jobs: [{ company: 'Google' }, { company: 'Facebook' }],
},
});
const fields = form.getValues().jobs.map((_, index) => (
<input
{...form.getInputProps(`jobs.${index}.company`)}
key={index}
/>
));
return <form>{fields}</form>;
}
// ✅ Correct: Add a wrapper element and pass key to it
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
jobs: [
{ company: 'Google', key: randomId() },
{ company: 'Facebook', key: randomId() },
],
},
});
const fields = form.getValues().jobs.map((item, index) => (
<div key={item.key}>
<input
{...form.getInputProps(`jobs.${index}.company`)}
key={form.key(`jobs.${index}.company`)}
/>
</div>
));
return <form>{fields}</form>;
}
커스텀 컴포넌트에서의 비제어 모드
비제어 폼 모드를 지원하는 커스텀 컴포넌트를 만들려면 defaultValue prop을 지원해야 해요. defaultValue를 지원하는 가장 좋은 방법은 use-uncontrolled 훅을 사용하는 것이에요:
import { useUncontrolled } from '@mantine/hooks';
interface CustomInputProps {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
}
// ✅ CustomInput supports both controlled and uncontrolled modes
function CustomInput({
value,
defaultValue,
onChange,
}: CustomInputProps) {
const [_value, handleChange] = useUncontrolled({
value,
defaultValue,
finalValue: 'Final',
onChange,
});
return (
<input
type="text"
value={_value}
onChange={(event) => handleChange(event.currentTarget.value)}
/>
);
}
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { text: 'Initial' },
});
// ✅ CustomInput supports `defaultValue` prop,
// it can be used in uncontrolled mode
return (
<CustomInput
{...form.getInputProps('text')}
key={form.key('text')}
/>
);
}
더 알아보기 (Learn more)
- use-field — use-field 훅
- Form values — 폼 값