Към съдържанието
wannacode

Формуляри в компонент

Накратко: Контролираното поле в React държи стойността си в състоянието (value и onChange), така че състоянието е единственият източник за проверката и изпращането.

Проблем

Написано, но празно

Купувачът попълва поръчката и натиска „Поръчай“. Сървърът отговаря, че името е задължително. А полето „Име“ показва името.

React

Напиши име, имейл и адрес, отметни условията и натисни „Поръчай“. Полето пази „Мария Иванова“, но в заявката заминава name: ''. Имейлът и адресът имат value и onChange, а името няма. Кодът изпраща състоянието, а то не знае какво е написано в полето.

Идеята

Една истина: състоянието

В урока за DOM формулярът пазеше текста сам и кодът го четеше при изпращане. В компонент има и второ място: състоянието. Ако двете не са свързани, всяко казва своето, както в примера.

Решението е полето да няма своя истина. То показва стойността от състоянието и при всеки клавиш казва на състоянието новия текст. Такова поле се казва контролирано (controlled input).

const [name, setName] = useState('');
<input id="name" value={name} onChange={(e) => setName(e.target.value)} />

value={name} е посоката от състоянието към полето. onChange е посоката обратно: e.target.value е текстът на полето в момента на събитието. В React onChange идва при всеки клавиш, не само когато полето загуби фокус.

Другите видове полета са същото, с друго свойство:

<select value={payment} onChange={(e) => setPayment(e.target.value)}>…</select>
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} />

Сега състоянието е единственият източник: каквото е в него, това е в полето, и обратното. Бутонът може да се изключи по състоянието (!agree), а заявката изпраща точно каквото вижда купувачът.

Виж го

Виж кръга

Една буква минава целия път: поле, събитие, състояние, ново рисуване и пак поле. После същото поле без връзка.

function CheckoutForm() {
const [name, setName] = useState('');
return (
<form onSubmit={submit}>
<label htmlFor="name">Име</label>
<input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</form>
);
}
Стъпка по стъпка, с думи
  1. Формулярът пази името в състоянието: name = ''. Полето на страницата показва каквото казва състоянието, а то казва: празно.
  2. Купувачът натиска М. Браузърът слага буквата в полето и пуска събитието input.
  3. onChange хваща събитието. e.target.value е текстът на полето сега: „М“.
  4. setName("М") сменя състоянието. Това е единственото място, където живее името.
  5. Състоянието се смени и React вика CheckoutForm наново. Тя връща полето с value = name: „М“.
  6. Полето получава стойността си от състоянието. Буквата е същата, но сега състоянието решава какво показва полето.
  7. Всеки клавиш минава целия кръг: поле, събитие, състояние, ново рисуване, поле. Един източник на истината: състоянието.
  8. Сега същото поле без value и onChange. Полето работи, но никой не го слуша и никой не му дава стойност.
  9. Купувачът пише „Мария“. Буквите остават в DOM-а. Състоянието още казва: name = ''.
  10. При изпращане кодът праща състоянието: { name: '' }. Сървърът отговаря „Името е задължително“, а полето показва „Мария“.
  11. Правилото: всяко поле има value от състоянието и onChange обратно към него. Тогава полето и състоянието не могат да се разминат.

Кръгът изглежда като излишна обиколка: буквата и без него стои в полето. Но заради него състоянието и полето не могат да се разминат.

Пробвай

Вържи полетата

Полето „Име“, падащият списък „Плащане“ и отметката „Приемам условията“ не са вързани за състоянието. Вържи и трите. Бутонът „Поръчай“ е изключен, докато условията не са приети. Проверката пише име, имейл и адрес, избира „С карта при доставка“, отметва и маха отметката и после поръчва. Очаква „Поръчка № 61 е приета.“ и заявка с написаното име и payment: 'card'.

React

Пробвай

Попълни

Какво стои на празното място, за да е вързана отметката?

const [agree, setAgree] = useState(false);

<input
  type="checkbox"
  checked={agree}
  onChange={(e) => setAgree(e.target.)}
/>

Пробвай

Намери грешката

Купувачът натиска клавиши в полето „Име“, а в него не се появява нищо. Имейлът работи. Кой ред е виновен?

Избери реда с грешката.

Идеята

Проверка до полето

Сървърът проверява всяка поръчка. Купувачът обаче не трябва да чака отговор, за да разбере, че е забравил адреса. Затова формулярът проверява и сам, преди да изпрати.

  1. Кога. При натискане на „Поръчай“. Ако има грешки, заявка не тръгва.
  2. Какво. Функция check() връща обект с грешките по поле: { name: 'Напиши името си', email: … }. Празен обект значи „всичко е наред“. Той се пази в състоянието errors.
  3. Къде. До полето, а не в общ списък горе. Купувачът вижда кое поле да поправи.
  4. За всеки. Екранният четец не вижда, че текстът е червен. Затова полето получава aria-invalid="true" (четецът казва „невалидно“), а съобщението получава id, към който полето сочи с aria-describedby. Когато фокусът дойде на полето, четецът прочита и съобщението.
<input
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && <p id="email-error" className="error">{errors.email}</p>}

Формулярът е с novalidate (в JSX noValidate). Без него браузърът спира изпращането със свои балончета, преди да стигне до твоя код, и двете проверки си пречат.

Сървърът проверява пак. Проверката в браузъра е за удобството на купувача. Тя не пази магазина: кодът на страницата е при купувача и заявка може да дойде и без нея. Защо е така има в урока „Недоверен вход“.

Пробвай

Провери преди изпращане

Добави проверка преди изпращане. Празно име дава „Напиши името си“, имейл без @ дава „Имейлът трябва да съдържа @“, празен адрес дава „Напиши адрес за доставка“. Всяко съобщение е до полето си, в елемент с id name-error, email-error и address-error, а полето има aria-invalid=true и aria-describedby към него. С грешки заявка не тръгва. Проверката отметва условията, поръчва празен формуляр, после с имейл без @ и накрая с всичко вярно.

React

Идеята

Изпращане и отговорът на сървъра

Заявката отнема време, а купувачът не знае дали кликът е стигнал. Ако кликне пак, заминава втора поръчка. Затова състоянието има и sending:

  1. Преди fetch sending става true. Бутонът е disabled, а до него пише „Изпраща се…“.
  2. След отговора sending става false, каквото и да е дошло.
  3. 201: в тялото е { id }. Формулярът се сменя със „Поръчка № 61 е приета.“
  4. 400: в тялото е { errors: { name, email, address, items } }, по едно съобщение на поле. Те отиват в същото errors, затова се показват на същите места до полетата. items няма поле: показва се до реда с кошницата.
setSending(true);
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, address, payment, items: ITEMS }),
});
const data = await res.json();
setSending(false);
if (res.ok) setOrderId(data.id);
else setErrors(data.errors);

Проверката в браузъра е по-проста от тази на сървъра: тя търси само „@“. Имейлът maria@shop я минава, а сървърът го връща с „Имейлът не е валиден“. Затова формулярът трябва да може да покаже и грешка, за която сам не е знаел.

По-точно, за любопитните

Неконтролирани полета. Поле без value пази текста само. Стойността се взима при изпращане: от FormData или с ref. defaultValue дава само началния текст. Това е по-малко код, но състоянието не знае какво пише купувачът, докато не изпрати. Затова не можеш да изключиш бутона по полето или да провериш, докато пише.

От неконтролирано към контролирано. Ако value е undefined в началото (useState() без начална стойност) и после стане текст, React казва в конзолата A component is changing an uncontrolled input to be controlled. Началната стойност на текстово поле е '', не undefined.

Формуляри с действие в React 19. <form action={fn}> вика fn с FormData на формуляра и след това изчиства неконтролираните полета. Има и куки около него (useActionState, useFormStatus). Това е друг стил, по-близо до формуляра от урока за HTML. Контролираните полета остават най-прекият път, когато интерфейсът трябва да реагира, докато купувачът пише.

Обект вместо пет състояния. Може и const [form, setForm] = useState({ name: '', email: '', … }) с една функция onChange, която чете e.target.name. Тогава новият обект е копие: setForm({ ...form, [e.target.name]: e.target.value }).

Кога да проверяваш. При изпращане е най-простото. Проверка при всеки клавиш показва грешки, преди купувачът да е довършил („имейлът е невалиден“ след първата буква). Честото решение е: първа проверка при изпращане или при напускане на полето (blur), а после при всеки клавиш, за да изчезне грешката, щом е поправена.

Фокус при грешка. След неуспешно изпращане е удобно фокусът да отиде на първото поле с грешка. Така клавиатурата и екранният четец стигат направо до проблема.

Проверката не може да види кода. Задачите тук проверяват поведението: какво се вижда, какво е изключено, какво заминава. Те не виждат дали проверката е в отделна функция или как са кръстени променливите.

Упражнение

Поръчката докрай

Сега формулярът проверява в браузъра, но не показва, че изпраща, а грешките на сървъра събира в един абзац. Направи го като истинска поръчка: докато заявката върви, бутонът „Поръчай“ е изключен и има текст „Изпраща се…“ (с role=status). При отговор 400 грешките от data.errors отиват до полетата, на същите места като твоите (email-error и aria-invalid на имейла), а грешката за items до реда с кошницата. Проверката пише имейл maria@shop: той минава твоята проверка, но сървърът го връща с „Имейлът не е валиден“. После добавя .bg и поръчката минава: „Поръчка № 61 е приета.“

React

Тест

Провери се

1. Полето „Име“ показва „Мария“, а състоянието name е празно. Кое заминава към сървъра?
2. Полето е <input value={name} />, без onChange. Какво става, когато купувачът пише?
3. Как се чете отметката в onChange?
4. Проверката в браузъра казва, че имейлът е наред. Трябва ли сървърът да го проверява пак?
5. Защо бутонът „Поръчай“ се изключва, докато заявката върви?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·