Irbisa · Шпаргалка

Формы и actions

Контролируемые и неконтролируемые поля, React Hook Form, валидация через Zod, actions в React 19, useActionState, useFormStatus, useOptimistic

Middle-разработчик · 21 вопрос

  1. 01

    Чем controlled-инпут отличается от uncontrolled в React?

    Controlled-инпут берёт value из стейта React и сообщает о каждом изменении через onChange, поэтому единственным источником правды остаётся стейт.

    function Controlled() {  const [name, setName] = useState('');  return <input value={name} onChange={(e) => setName(e.target.value)} />;} function Uncontrolled() {…
  2. 02

    Как прочитать все значения формы при сабмите, не храня каждое поле в стейте?

    Дайте каждому полю name, обработайте onSubmit, вызовите e.preventDefault() и создайте new FormData(e.currentTarget).

    function SignupForm({ onSave }: { onSave: (v: Record<string, FormDataEntryValue>) => void }) {  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {    e.preventDefault(); // stop the full page reload    const data = new FormData(e.currentTarget);    onSave(Object.fromEntries(data));  }…
  3. 03

    Как сделать controlled чекбокс, группу радиокнопок и <select> в React?

    У каждого контрола свой проп для значения: чекбокс использует checked и читает e.target.checked, радиокнопка получает checked={value === option}, а <select> принимает value на самом <select>, а не selected на <option>.

    type Plan = 'free' | 'pro'; function Settings() {  const [news, setNews] = useState(false);  const [plan, setPlan] = useState<Plan>('free');  const [country, setCountry] = useState('kz');…
  4. 04

    Найдите баг: при вводе в поле в консоли появляется «A component is changing an uncontrolled input to be controlled».

    function NameField() {  const [name, setName] = useState<string>();   return (    <input      value={name}      onChange={(e) => setName(e.target.value)}      placeholder="Your name"    />  );}

    Стейт начинается с undefined, поэтому на первом рендере у инпута нет value и React считает его uncontrolled; после первого символа value становится строкой, и инпут переключается в controlled.

  5. 05

    Почему <input type="file"> в React всегда неконтролируемый и как показать превью выбранного файла и загрузить его?

    Из соображений безопасности выбрать файл может только пользователь: скрипт не может задать value у поля выбора файла (разве что очистить его), поэтому React нечего контролировать.

    function AvatarUpload() {  const [preview, setPreview] = useState<string | null>(null);  const [status, setStatus] = useState<'idle' | 'uploading' | 'done' | 'failed'>('idle');   // frees the previous object URL when it changes, and the last one on unmount  useEffect(() => () => { if (preview) URL.revokeObjectURL(preview); }, [preview]);…
  6. 06

    Как валидировать форму в React и когда запускать валидацию: при изменении, при потере фокуса или при сабмите?

    Валидируйте на клиенте ради быстрого фидбэка и всегда повторно на сервере, потому что клиентские проверки можно обойти.

  7. 07

    Зачем нужен React Hook Form вместо useState на каждое поле и как он сокращает ререндеры?

    React Hook Form регистрирует инпуты как uncontrolled и читает их значения из DOM через ref, поэтому ввод текста не ререндерит компонент формы.

    import { useForm } from 'react-hook-form'; type Login = { email: string; password: string }; function LoginForm({ onLogin }: { onLogin: (v: Login) => Promise<void> }) {  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<Login>();…
  8. 08

    Как валидировать форму схемой Zod в React Hook Form и что это даёт?

    Опишите форму схемой Zod, передайте zodResolver(schema) в useForm и выведите тип TypeScript через z.infer.

    import { useForm } from 'react-hook-form';import { zodResolver } from '@hookform/resolvers/zod';import { z } from 'zod'; const schema = z  .object({…
  9. 09

    Что такое Controller в React Hook Form и когда он нужен вместо register?

    Controller оборачивает controlled-компонент без нативного инпута, например date picker, react-select или компонент MUI, и связывает его value и onChange с формой.

    import { Controller, useForm } from 'react-hook-form';import Select from 'react-select'; type Option = { value: string; label: string };type Form = { city: Option | null };…
  10. 10

    Как сделать в React Hook Form форму с динамическим списком полей (добавление и удаление строк)?

    Используйте useFieldArray({ control, name }): он возвращает fields для рендера и append, remove, insert, move для изменения списка, а значения, ошибки и фокус остаются привязаны к нужной строке.

    import { useFieldArray, useForm } from 'react-hook-form'; type Invoice = { items: { name: string; qty: number }[] }; function InvoiceForm() {  const { control, register, handleSubmit } = useForm<Invoice>({…
  11. 11

    Найдите баг: форма редактирования показывает пустые поля, хотя данные пользователя уже загрузились.

    function EditProfile({ userId }: { userId: string }) {  const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId) });  const { register, handleSubmit } = useForm<ProfileValues>({    defaultValues: { name: user?.name ?? '', email: user?.email ?? '' },  });   return (    <form onSubmit={handleSubmit(saveProfile)}>      <input {...register('name')} />      <input {...register('email')} />      <button>Save</button>    </form>  );}

    useForm читает defaultValues один раз, на первом рендере, когда user ещё undefined, поэтому форма остаётся с пустыми строками и после того, как запрос завершился.

  12. 12

    Как сделать многошаговую форму (мастер, wizard) и где хранить её состояние и валидацию между шагами?

    Держите одно состояние формы для всех шагов выше компонентов шагов, чтобы значения переживали размонтирование шага, и перед переходом дальше проверяйте только поля текущего шага.

    const signupSchema = z.object({  email: z.email(),  password: z.string().min(8),  name: z.string().min(1),  city: z.string().min(1),});…
  13. 13

    Как сделать ошибки формы доступными для пользователей скринридеров и клавиатуры?

    Свяжите каждую ошибку с её полем: поставьте инпуту aria-invalid, направьте aria-describedby на элемент с текстом ошибки, дайте каждому полю настоящий <label>, а после неудачного сабмита переведите фокус на первое невалидное поле.

    function EmailField({ error }: { error?: string }) {  const id = useId();  const errorId = `${id}-error`;   return (    <div>…
  14. 14

    Что такое Actions в React 19 и что происходит, если передать функцию в <form action>?

    Action представляет собой асинхронную функцию, которую React выполняет внутри транзишена и сам отслеживает её pending-состояние, ошибки и оптимистичные обновления.

    function NewsletterForm({ subscribe }: { subscribe: (email: string) => Promise<void> }) {  async function action(formData: FormData) {    await subscribe(String(formData.get('email')));    // on success React clears the uncontrolled email field  }…
  15. 15

    Что возвращает useActionState и как с его помощью показывать ошибки валидации из form action?

    useActionState(action, initialState) возвращает [state, formAction, isPending].

    type State = { error?: string; ok?: boolean }; function RenameForm({ rename }: { rename: (name: string) => Promise<void> }) {  const [state, formAction, isPending] = useActionState<State, FormData>(    async (_prev, formData) => {      const name = String(formData.get('name')).trim();…
  16. 16

    Найдите баг: кнопка сабмита никогда не переключается на «Saving...», пока выполняется form action.

    import { useFormStatus } from 'react-dom'; function CommentForm({ post }: { post: (fd: FormData) => Promise<void> }) {  const { pending } = useFormStatus();   return (    <form action={post}>      <textarea name="text" />      <button disabled={pending}>{pending ? 'Saving...' : 'Post'}</button>    </form>  );}

    useFormStatus вызывается в том же компоненте, который рендерит <form>, а он сообщает статус только родительской формы, поэтому здесь pending всегда false.

  17. 17

    Что увидит пользователь в каждом поле после успешного завершения этого form action?

    function PostForm({ save }: { save: (fd: FormData) => Promise<void> }) {  const [tag, setTag] = useState('');   return (    <form action={save}>      <input name="title" defaultValue="Draft" />      <input name="tag" value={tag} onChange={(e) => setTag(e.target.value)} />      <button>Save</button>    </form>  );}// The user types "Hooks" into title and "react" into tag, then clicks Save.

    Поле title вернётся к «Draft», а поле tag сохранит «react».

  18. 18

    Как работает useOptimistic и что происходит с оптимистичным значением, если action упал?

    useOptimistic(state, updateFn) возвращает значение, равное state, пока вы не вызовете сеттер внутри action; после этого он сразу показывает ваше предполагаемое значение, пока этот action выполняется.

    type Msg = { text: string; sending?: boolean }; function Chat({ messages, send }: { messages: Msg[]; send: (text: string) => Promise<void> }) {  const [optimistic, addOptimistic] = useOptimistic(    messages,    (list: Msg[], text: string) => [...list, { text, sending: true }],…
  19. 19

    Как защитить форму, работающую с API, от двойного сабмита и ответов, пришедших не в том порядке?

    Блокируйте второй сабмит, пока первый в процессе, и сделайте сервер устойчивым к дублям, которые всё равно прорвутся: отключайте кнопку по isPending или isSubmitting и отправляйте ключ идемпотентности, чтобы повторный запрос не создал второй заказ.

  20. 20

    Большая форма тормозит на каждое нажатие клавиши. Как найти причину и исправить?

    Обычно на каждое нажатие ререндерится вся форма, потому что значения всех полей лежат в одном объекте стейта наверху.

  21. 21

    Когда достаточно form Actions из React 19, а когда всё же стоит взять React Hook Form?

    Actions с useActionState закрывают формы, правила которых проверяются при сабмите, особенно вместе с server functions, где форма работает даже до загрузки JavaScript.