Головна / Статті / Сформована форма з датою народження у Next.js із контрольованими введеннями та калебами

Сформована форма з датою народження у Next.js із контрольованими введеннями та калебами

Створіть невелику клієнтську форму на Next.js, яка відстежує введення за допомогою useState, відхиляє некоректні дати народження, відображає зручні для сприйняття повідомлення про помилки та передає очищені дані своєму батьківському компоненту.

2384 слів

Додаток з гороскопами потребує від користувача двох речей, щоб створити прогноз: ім’я та дату народження. Це здається формою, яку можна заповнити за п’ять хвилин, але навіть цей невеликий елемент вимагає серйозних дизайнерських рішень: де він буде функціонувати у додатку Next.js, хто буде володіти введеними даними, як будуть відхилятися недійсні дати та хто вирішуватиме, що робити після успішної передачі даних.

У цьому посібнику ми будуватимемо цю форму крок за кроком. До кінця ви отримаєте компонент форми, який перевіряє введені дані, зручно повідомляє про помилки та передає лише коректні дані далі, а також чітку модель, яку можна буде використовувати для будь-якої форми, що складається з кількох полів.

Визначте, за що відповідає компонент

Перш ніж писати будь-який JSX, варто перелічити завдання компонента. У цій формі їх рівно три:

  1. Зберігати те, що ввів користувач.
  2. Перевіряти введені дані перед їх надсиланням.
  • Передавайте коректні дані назад до батьківського компонента.
  • Усе, що не входить до цього списку, наприклад виклик API чи відображення результату, належить до іншої частини коду. Короткий список робить решту дизайну простішою для розуміння.

    Чому форма мусить бути клієнтським компонентом

    У App Router кожен компонент є серверним, якщо тільки ви не виберете інший варіант. Серверні компоненти відображаються на сервері та не містять інтерактивного JavaScript, тому вони не можуть зберігати стан чи реагувати на події. Тому форма починається з директиви client.

    "use client";
    

    Компонент залежить від кількох елементів, які існують лише в браузері:

    • useState для зберігання поточних значень,
    • обробників onChange для полів введення,
    • обробника onSubmit для форми,
    • постійної взаємодії з користувачем,
  • Перевірка, яка виконується перед надсиланням будь-чого.
  • Коротко кажучи, компонент не просто відображає інформацію; він має реагувати на дії користувача. Саме це є ознакою для позначення його як клієнтського компонента. Корисною практикою є зберігання таких компонентів невеликими та розташованими на кінцях ієрархії, щоб директива не включала великі частини сторінки до клієнтського пакету.

    Введіть дані та параметри

    Форма імпортує спільний тип Profile разом із useState.

    import { Profile } from "../types";
    import { useState } from "react";
    

    Явне оголошення форми надсиланих даних означає, що TypeScript може перевіряти кожне місце, де вони створюються або використовуються, замість того щоб дозволяти довільному об’єкту проходити через програму.

    export type Profile ={
     name: string;
     dob: string;
    }
    

    Далі йдуть параметри компонента. Їх лише один: функція-колбек, яку надає батьківський компонент.

    type HoroscopeFormProps = {
      onSubmit: (info: Profile) => void;
    };
    

    Нехай батьківський компонент вирішує, що робити далі

    Саме тут малюється межа компонента. Форма збирає дані, але не має права вирішувати, що з ними робити. Залежно від екрана батьківський компонент може:

    • звернутися до API,
    • сгенерувати гороскоп,
    • зберегти профіль,
    • показати результат,
    • оновити інший стан.

    Якщо будь-який з цих варіантів закодувати безпосередньо в формі, це прив’яже її до одного екрана. Натомість використання функції onSubmit дозволяє їй бути повторно використаною. Тип цієї функції вказує, що вона отримує об’єкт типу Profile та нічого не повертає (void), тож форма її виконує та продовжує роботу. Загальний алгоритм виглядає так:

    User enters information
            ↓
    HoroscopeForm collects it
            ↓
    HoroscopeForm validates it
            ↓
    onSubmit(user)
            ↓
    Parent decides what happens next
    

    Кожен крок має свого власника, а роль форми закінчується у момент виклику копі-функції.

    Зберігати введені дані у стані React

    Форма потребує місця для зберігання поточних значень. Три елементи стану покривають усе.

    const [name, setName] = useState<string>("");
    const [dob, setDob] = useState<string>("");
    const [error, setError] = useState<string>("");
    

    Спочатку подивіться на назву.

    const [name, setName] = useState<string>("");
    

    useState повертає пару. name — це поточне значення, а setName — функція, яку ви викликаєте для його заміни; вона також запускає перерендеринг. Початкове значення — це порожній рядок, оскільки ще нічого не було введено. Використання рядка замість undefined має значення для керованих полів введення: React попереджає, якщо поле переходить від некерованого до керованого, коли його value змінюється з undefined на рядок.

    Дата народження слідує тій самій схемі.

    const [dob, setDob] = useState<string>("");
    

    Останній елемент зберігає поточне повідомлення про помилку.

    const [error, setError] = useState<string>("");
    

    Порожня рядок означає, що наразі немає помилок. Функція валідації записуватиме повідомлення у цей стан, коли виникне проблема, та очистить його після того, як вхідні дані будуть прийняті.

    Зберігайте валідацію дати у окремій функції

    Правила валідації зазвичай зростають у кількості, тому замість того, щоб накопичувати їх у обробнику подання даних, перевірка дати виконується у спеціалізованій функції. Її сигнатура визначає умови використання.

    function validateDOB(dob: string): string | null {
    

    Існує рівно два результати. Недійсна дата повертає рядок із поясненням проблеми; дійсна дата повертає null.

    Valid date
       ↓
    return null
    
    Invalid date
       ↓
    return error message
    

    Оскільки функція відповідає на одне запитання — «Чи є ця дата народження прийнятною?» — її легко читати, легко тестувати як окремий модуль без відображення будь-чого, а також легко повторно використовувати на сервері, якщо пізніше перевіряти дані там також. Повернення повідомлення замість кидання винятку робить код, який його викликає, простішим: достатньо перевірити результат та відобразити його у разі наявності.

    Які дати слід відхилити

    Для дати народження доцільні два правила:

    • Без майбутніх дат. Людина не може народитися у день, який ще не настав.
    • Розумні нижні межі. Дати, які належать до 150 років тому, відхиляються, оскільки майже напевно є результатом помилки введення.

    Дата здається простою, поки не враховується час доби. Елемент <input type="date"> повертає рядок у форматі YYYY-MM-DD, а оператор new Date("2024-05-01") інтерпретує цей рядок як північ ночі за UTC, тоді як значення „сьогодні“, створене за допомогою new Date(), враховує місцевий час у годинах, хвилинах та секундах. Залежно від часової зони користувача наївне порівняння може помилково визнати завтрашній день або відхилити сьогоднішній. Два надійні варіанти — це нормалізувати обидві сторони до початку дня перед порівнянням або безпосередньо порівнювати рядки у форматі YYYY-MM-DD, які правильно сортуються як текст. Який би підхід ви не обрали, та чи допомагав вам у цьому штучний інтелект чи ні, переконайтеся, що можете пояснити, навіщо потрібне кожне порівняння; помилки з датами ховаються саме в тих рядках, які ніхто не зрозумів.

    Координуйте все у обробнику форми

    Коли стан та валідація готові, handleSubmit поєднує їх. Коли користувач надсилає форму, ця функція повинна:

    1. зупинити стандартну дію браузера щодо надсилання форми, яка призведе до перезавантаження чи переміщення на сторінку,
    2. переконатися, що в обох полях є значення,
    3. перевірити дату народження,
    4. показати помилку, якщо щось не так,
    5. інакше передати дані батьківському елементу.

    Вона починається ось так.

    const handleSubmit = (e: React.SubmitEvent) => {
      e.preventDefault();
    

    За замовчуванням надсилання форми відправляє запит та перезавантажує сторінку. Оскільки тут обробкою надсилання займається React, цей стандартний режим потрібно скасувати.

    e.preventDefault();
    

    З цього моменту сам компонент вирішує, що буде робитися під час надсилання даних. Примітка щодо типу події: багато проектів задають цей параметр як React.FormEvent<HTMLFormElement>. Перевірте, які типи подій надсилання підтримує ваша встановлена версія @types/react, та оберіть той, який послідовно використовується у вашому проекті.

    Відхилення порожніх полів за допомогою раннього повернення

    Перш ніж перевіряти, чи має сенс вказана дата, переконайтеся, що щось узагалі було введено.

    if (!name || !dob) {
      setError("Please enter in information");
      return;
    }
    

    Якщо будь-яке з полів порожнє, обробник фіксує помилку та негайно повертається. Це є шаблоном раннього повернення (або клозу-захисника): як тільки стає відомо, що вхідні дані недійсні, більше немає чого робити, тому функція завершується замість того, щоб обгортати решту логіки в інший рівень блоків if. Кожен захисник обробляє одну помилку, а „правильний шлях“ залишається простим у кінці.

    Виконати перевірку дати

    Як тільки стає відомо, що дата існує, вона проходить через верифікатор.

    const dobError = validateDOB(dob);
    

    Результатом є або повідомлення, або null, тому достатньо однієї перевірки.

    if (dobError) {
      setError(dobError);
      return;
    }
    

    Повідомлення означає, що обробник його відображає та зупиняється. null означає, що дата пройшла перевірку, і виконання продовжується.

    Передати чисті дані батьківському елементу

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

    setError("");
    

    Потім функція-відповідач батьківського елемента отримує перевірений профіль.

    onSubmit({ name, dob });
    

    Це результат попереднього рішення щодо проектування. Форма не знає та не цікавиться тим, що відбуватиметься далі; вона просто повідомляє про наявність коректних даних, а батьківський елемент вирішує, що робити. Той самий компонент може сьогодні подавати дані для генератора гороскопу, а завтра — на екран налаштувань профілю без змін.

    Під’єднати логіку до маркапу

    Елемент форми пов’язує надсилання даних із обробником.

    <form onSubmit={handleSubmit}>
    

    Це наказує React виконувати handleSubmit щоразу, коли форма надсилається, чи то шляхом натискання кнопки, чи через натиснення Enter у полі. Далі йде поле з ім’ям.

    <input
      type="text"
      value={name}
      onChange={(e) => setName(e.target.value)}
    />
    

    Як контрольоване поле вводу залишається синхронним

    Це є контрольованим введенням: джерелом істини для його значення є стан React, а не DOM. Кожного разу, коли користувач вводить текст, запускається обробник змін.

    onChange={(e) => setName(e.target.value)}
    

    Він читає новий текст з події та зберігає його у стані. Повний цикл виглядає так:

    User types
        ↓
    onChange fires
        ↓
    setName(new value)
        ↓
    name state updates
        ↓
    value={name}
        ↓
    Input displays updated value
    

    Оскільки поле введення завжди відображає те, що міститься у name, значення, яке ви перевіряєте, гарантовано є тим самим значенням на екрані. Поле з датою використовує ту саму схему.

    <input
      type="date"
      value={dob}
      onChange={(e) => setDob(e.target.value)}
    />
    

    Стан відстежує обрану дату, і кожна зміна викликає setDob. Ще одним варіантом, який варто розглянути, є використання вбудованого атрибута max, встановленого на сьогоднішню дату, що заважає більшості інструментів вибору дат пропонувати майбутні дні, водночас ваш перевірювач все одно захищає від неправильного введення та старіших браузерів.

    Кожен елемент введення також має мати видимий <label>, пов’язаний з ним. Замінник або сусідній заголовок не є альтернативою; саме лейбл оголошується екранними читачами та дозволяє зробити поле клікабельним завдяки його підпису.

    Відображати помилки лише тоді, коли вони існують

    Повідомлення про помилку має з’являтися лише у разі її наявності. Для цього використовується умовне відображення.

    {error && (
      <p role="alert">
        {error}
      </p>
    )}
    

    Коли змінна error містить текст, абзац відображається; коли це порожня стрічка, яка вважається хибною, нічого не з’являється. Тут короткий шлях && є безпечним, оскільки значення — це стрічка. З числами це може призвести до проблем: значення 0 буде відображено як літеральне „0“.

    Абзац також має роль ARIA.

    role="alert"
    

    role="alert" повідомляє технології допомоги користувачеві, що цей контент є важливим та часочутливим, тому скрін-читери оголошують про нього відразу після його з’явлення. Це зміна лише одного атрибута, яка робить повідомлення про перевірку корисним для людей, які не бачать вікна з повідомленням. Для ще більшої зрозумілості ви також можете позначити проблемне поле атрибутом aria-invalid та посилатися на повідомлення за допомогою aria-describedby.

    Додайте кнопку надсилання

    Останнім елементом є кнопка, яка прямо визначена як кнопка надсилання.

    <button type="submit">
      Submit
    </button>
    

    Усередині форми кнопка з параметром type="submit" запускає функцію onSubmit форми, а разом з нею — handleSubmit. Кнопки в формі за замовчуванням все одно призначені для надсилання даних, але вказування типу запобігає несподіванкам, якщо хтось пізніше додасть другу кнопку, призначену для іншої мети, наприклад для очищення полів.

    Повний потік даних

    Якщо дивитися в цілому, компонент передає дані в одному напрямку:

    State
      ↓
    User input
      ↓
    Submit
      ↓
    Validation
      ↓
    Parent callback
    
    • useState зберігає те, що ввів користувач.
    • Поле введення оновлює цей стан після кожної зміни.
    • Надсилання форми запускає функцію handleSubmit.
    • handleSubmit перевіряє значення.
    • Некоректні дані встановлюють стан помилки та зупиняють процес.
    • Коректні дані надсилаються до батьківського компонента через onSubmit, і батьківський компонент їх отримує.

    Куди рухатися далі

    Цей метод ручної налаштуваності ідеальний для навчання та цілком підходить для форм із двома полями. Коли кількість полів у формі зростає, а також потрібні правила між полями чи перевірки на сервері, варто використовувати бібліотеку схем, щоб однакові правила працювали як на клієнті, так і на сервері; один із способів це зробити — поділитися схемою Zod між фронтендом на React та бекендом на Node. Перевірка на клієнтській стороні покращує користувацький досвід, але ніколи не замінює перевірку на сервері, адже будь-який запит можна створити вручну.

    Основні висновки

    • Позначайте лише інтерактивні компоненти атрибутом "use client" та робіть їх невеликими.
    • Надайте формі одну функцію: збирати дані, перевіряти їх та передавати далі. Нехай батьківський компонент керує побічними ефектами через типований калебек.
  • Використовуйте контрольовані введення, ініціалізовані рядками, щоб значення на екрані збігалося з тим, яке ви перевіряєте.
  • Розміщуйте правила перевірки у чистих функціях, які повертають повідомлення або null; їх легко тестувати та повторно використовувати.
  • Обережно ставіться до дат: нормалізуйте час доби або порівнюйте рядки у форматі YYYY-MM-DD, щоб уникнути помилок через різницю часових зон.
  • Використовуйте ранні повернення, щоб зробити обробник надсилання простим, а також role="alert" та належні мітки, щоб помилки залишалися доступними.
  • Здатність пояснити, чому існує кожен рядок, особливо ті, які запропонував штучний інтелект, є частиною завершення роботи.