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)