apps/mantine.dev/src/pages/form/uncontrolled.mdx
import { FormDemos } from '@docs/demos'; import { Layout } from '@/layout'; import { MDX_DATA } from '@/mdx';
export default Layout(MDX_DATA.formUncontrolled);
Controlled mode is the default mode of the form. In this mode, the form data is stored in React state and all components are rerendered when the form data changes. Controlled mode is not recommended for large forms.
Example of a form with controlled mode:
<Demo data={FormDemos.controlled} />As you can see in the example above, form.values update on every change. This
means that every component that uses form.values will rerender on every change.
Uncontrolled mode is an alternative form mode introduced in the 7.8.0 release. It is now the recommended mode for all forms. Uncontrolled mode provides significant performance improvements for large forms.
With uncontrolled mode, the form data is stored in a ref instead of React state
and form.values are not updated on every change.
Example of a form with uncontrolled mode:
<Demo data={FormDemos.uncontrolled} />As you can see in the example above, form.values do not update at all.
The form.getValues function returns the current form values. It can be
used anywhere in the component to get the current form values. It can
be used in both controlled and uncontrolled modes.
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' }
Although form.values can be used to get the current form values in controlled mode, it is
recommended to use form.getValues instead as it always returns the latest
values while form.values is outdated in uncontrolled mode and before the state
update in controlled mode.
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() returns a ref value of the current form values. This means that
you cannot pass it to the useEffect dependencies array as it will always be the same
reference.
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()]);
Instead of observing form values with useEffect, use the onValuesChange callback
to listen to form value changes:
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);
},
});
form.getInputProps returns different props for controlled
and uncontrolled modes. In controlled mode, the returned object has value prop,
while in uncontrolled mode it has defaultValue prop.
Uncontrolled mode relies on the key returned from form.key() to update
components when form.setFieldValue or form.setValues are called. You should
set the key supplied by form.key() to the input component to ensure that it has
the updated value:
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { text: '' },
});
return (
<input {...form.getInputProps('text')} key={form.key('text')} />
);
}
In case you need to have a list of fields,
do not pass the key to the input component directly. Instead, add a wrapper
element and pass the key to it:
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>;
}
If you want to build a custom component that supports uncontrolled form mode,
you must add support for the defaultValue prop. The best way to add support for
defaultValue is to use the use-uncontrolled hook:
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')}
/>
);
}