Strona główna / Artykuły / Weryfikowany formularz z datą urodzenia w Next.js z kontrolowanymi polami wejściowymi i funkcjami zwrotnymi

Weryfikowany formularz z datą urodzenia w Next.js z kontrolowanymi polami wejściowymi i funkcjami zwrotnymi

Stwórz małą formę kliencką w Next.js, która śledzi wprowadzane dane za pomocą useState, odrzuca niewłaściwe daty urodzenia, wyświetla dostępne komunikaty błędów i przekazuje czyste dane do komponentu nadrzędnego.

2384 słów

Aplikacja horoskopowa potrzebuje od użytkownika dwóch rzeczy, zanim będzie mogła wygenerować prognozę: imienia i daty urodzenia. Wydaje się to formularzem trwającym pięć minut, ale nawet ten mały element wymusza ważne decyzje projektowe: gdzie ma działać w aplikacji Next.js, kto jest właścicielem wprowadzonych wartości, jak odrzucać nieważne daty oraz kto decyduje o dalszych działaniach po udanym wysłaniu.

To przewodnictwo buduje ten formularz krok po kroku. Pod koniec będziesz miał skomponowany, kontrolowany komponent formularza, który weryfikuje wprowadzone dane, wyraźnie informuje o błędach i przekazuje tylko poprawne dane dalej, a także jasny model mentalny, który możesz wykorzystać ponownie dla każdego formularza składającego się z więcej niż jednego pola.

Decyduj, za co odpowiada komponent

Zanim napiszesz jakikolwiek kod JSX, warto wymienić zadania tego komponentu. Ten formularz ma dokładnie trzy:

  1. Pamiętać to, co wpisał użytkownik.
  2. Sprawdzać dane przed ich wysłaniem.
  • Zwracaj ważne dane z powrotem do komponentu nadrzędnego.
  • Wszystko poza tą listą, takie jak wywoływanie API lub renderowanie wyników, należy umieścić gdzie indziej. Krótka lista sprawia, że reszta projektu jest prostsza w zrozumieniu.

    Dlaczego formularz musi być komponentem klienckim

    W App Routerze każdy komponent jest komponentem serwerowym, chyba że wybierzesz inaczej. Komponenty serwerowe są renderowane na serwerze i nie zawierają interaktywnego JavaScriptu, więc nie mogą przechowywać stanu ani reagować na zdarzenia. Dlatego formularz musi być oparty na dyrektywie klienckiej.

    "use client";
    

    Komponent ten zależy od kilku elementów, które istnieją tylko w przeglądarce:

    • useState do przechowywania aktualnych wartości,
    • obsługi zdarzeń onChange w polach wprowadzania danych,
    • obsługi zdarzenia onSubmit w formularzu,
    • ciągłej interakcji z użytkownikiem.
  • weryfikacja, która odbywa się przed wysłaniem jakichkolwiek danych.
  • Krótko mówiąc, komponent nie tylko wyświetla informacje; musi reagować na działania użytkownika. To właśnie jest sygnałem do oznaczenia go jako komponentu klienckiego. Praktycznym nawykiem jest utrzymywanie takich komponentów w małej formie i umieszczanie ich na końcach drzewa struktury, aby dyrektywa nie włączała dużych części strony do pliku klienckiego.

    Wpisz dane i atrybuty

    Formularz importuje wspólny typ Profile wraz z useState.

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

    Jawna deklaracja struktury przesyłanych danych oznacza, że TypeScript może sprawdzać każde miejsce, gdzie są one generowane lub wykorzystywane, zamiast pozwalać dowolnemu obiektowi przepływać przez aplikację.

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

    Następnie przychodzą atrybuty komponentu. Jest tylko jeden: funkcja zwrotna dostarczana przez komponent nadrzędny.

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

    Pozwól komponentowi nadrzędnemu zdecydować, co nastąpi dalej

    To właśnie tutaj rysowana jest granica komponentu. Formularz zbiera dane, ale nie powinien decydować o tym, co z nimi zrobić. W zależności od ekranu rodzicowy komponent może:

    • wywołać API,
    • wygenerować horoskop,
    • przechować profil,
    • pokazać wynik,
    • aktualizować inny stan.

    Jednostronne zapisanie którychkolwiek z tych opcji wewnątrz formularza przywiązałoby go do jednego ekranu. Zamiast tego przyjęcie funkcji onSubmit umożliwia jej ponowne użycie. Typ wskazuje, że onSubmit otrzymuje obiekt typu Profile i nie zwraca niczego (void), więc formularz ją wywołuje i przechodzi dalej. Ogólny przebieg wygląda następująco:

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

    Każdy krok ma jednego właściciela, a rola formularza kończy się w momencie wywołania funkcji zwrotnej.

    Zapisz dane wejściowe w stanie React

    Formularz potrzebuje miejsca na przechowywanie obecnych wartości. Trzy elementy stanu wystarczą do tego celu.

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

    Najpierw spójrz na nazwę.

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

    useState zwraca parę. name to aktualna wartość, a setName to funkcja, którą wywołujemy, aby ją zastąpić – ona również planuje ponowne renderowanie. Początkowa wartość to pusta strona, ponieważ nic jeszcze nie zostało wpisane. Inicjalizacja wartością stringiem zamiast undefined ma znaczenie w przypadku kontrolowanych pól wejściowych: React ostrzega, gdy pole wejściowe zmienia się z niekontrolowanego na kontrolowany, a jego value zmienia się z undefined na string.

    Data urodzenia follows the same pattern.

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

    Ostatni element przechowuje aktualną wiadomość o błędzie.

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

    Pusta strona oznacza, że w tym momencie nie ma błędu. Walidacja zapisa tam wiadomość, gdy coś pójdzie nie tak, a usunie ją po sprawdzeniu poprawności danych.

    Zachowaj walidację daty w osobnej funkcji

    Zasady walidacji mają tendencję do rozrastania się, dlatego zamiast gromadzić je w funkcji obsługującej wysłanie danych, sprawdzenie daty znajduje się w dedykowanej funkcji. Jej sygnatura wskazuje na zasady działania.

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

    Istnieją dokładnie dwa możliwe wyniki. Nieprawidłowa data generuje strugę opisującą problem; prawidłowa data zwraca null.

    Valid date
       ↓
    return null
    
    Invalid date
       ↓
    return error message
    

    Ponieważ funkcja odpowiada na jedno pytanie, „czy ta data urodzenia jest do przyjęcia?”, jest łatwa do odczytania, łatwa do testowania jednostkowego bez wyświetlania żadnych treści, a także łatwa do ponownego użycia na serwerze, jeśli później będzie tam również weryfikowana. Zwracanie komunikatu zamiast rzucania błędu utrzymuje prostotę kodu wywołującego: wystarczy sprawdzić wynik i go pokazać, jeśli jest dostępny.

    Które daty należy odrzucić

    Dla daty urodzenia sensowne są dwa zasady:

    • Brak dat przyszłych. Nikt nie może urodzić się w dniu, który jeszcze nie nadszedł.
    • Rozsądna dolna granica. Daty sprzed ponad 150 lat są odrzucane, ponieważ prawie na pewno zawierają błędy wprowadzenia.

    Data wydają się proste, dopóki nie weźmiemy pod uwagę pory dnia. Element <input type="date"> zwraca ciąg w formacie YYYY-MM-DD, a wyrażenie new Date("2024-05-01") interpretuje ten ciąg jako północ UTC, natomiast wartość „dzisiaj” utworzona za pomocą new Date() zawiera lokalne godziny, minuty i sekundy. W zależności od strefy czasowej użytkownika naiwna porównanie może błędnie przyjąć jutro lub odrzucić dzisiaj. Dwie skuteczne opcje to normalizacja obu wartości na początek dnia przed porównaniem lub bezpośrednie porównanie ciągów w formacie YYYY-MM-DD, które sortują się poprawnie jako tekst. Niezależnie od wybranej metody oraz od tego, czy pomógł ci w jej opracowaniu asystent AI, upewnij się, że potrafisz wyjaśnić, dlaczego każde porównanie jest konieczne; błędy związane z datami kryją się właśnie w tych fragmentach, których nikt nie zrozumiał.

    Koordynuj wszystko w obsłudze przesłania

    Gdy stan i walidacja są gotowe, handleSubmit łączy je ze sobą. Gdy użytkownik wysyła dane, funkcja ta musi:

    1. Zatrzymać standardowe działanie przeglądarki przy wysyłaniu danych, które spowodowałoby ponowne załadowanie lub przeglądanie strony,
    2. Potwierdzić, że oba pola zawierają wartości,
    3. Walidować datę urodzenia,
    4. Pokazać błąd, jeśli coś jest nie tak,
    5. w przeciwnym razie przekazać dane do komponentu nadrzędnego.

    Zaczyna się to w ten sposób.

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

    Domyślnie wysyłanie danych z formularza generuje żądanie i ponownie załadowuje stronę. Ponieważ to React zajmuje się wysyłaniem danych, ten domyślny zachowanie musi zostać anulowany.

    e.preventDefault();
    

    Od tego momentu to sam komponent decyduje, co ma zrobić po wysłaniu danych. Uwaga dotycząca typu zdarzenia: wiele baz kodu definiuje ten parametr jako React.FormEvent<HTMLFormElement>. Sprawdź, jakie typy zdarzeń submit są dostępne w zainstalowanej wersji @types/react, i wybierz ten, który jest spójnie używany w twoim projekcie.

    Odrzucenie pustych pól poprzez wczesne zwrócenie

    Zanim sprawdzisz, czy data ma sens, upewnij się najpierw, że w ogóle zostało coś wpisane.

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

    Jeśli któreś z pól jest puste, obsługa rejestruje błąd i natychmiast wraca. To jest wzorzec wczesnego powrotu (lub klauzuli ochronnej): gdy wiadomo, że dane wejściowe są nieważne, nie ma już nic do zrobienia, więc funkcja kończy swoją pracę zamiast umieszczać pozostałą logikę w kolejnym poziomie bloków if. Każda klauzula ochronna obsługuje jeden przypadek błędu, a „szlak prawidłowy” pozostaje prosty na końcu.

    Zastosuj sprawdzenie daty

    Gdy ustalono, że data istnieje, przechodzi ona przez walidator.

    const dobError = validateDOB(dob);
    

    Rezultatem jest albo komunikat, albo null, więc wystarczy jedna sprawdzenie.

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

    Komunikat oznacza, że obsługa go wyświetla i przestaje. null oznacza, że data została przyjęta i wykonywanie kontynuuje się.

    P przekaż czyste dane do elementu nadrzędnego

    Doskoczenie do tego punktu oznacza, że wszystkie sprawdzenia przeszły pomyślnie, więc wszelkie stare błędy z wcześniejszych prób zostały usunięte.

    setError("");
    

    Następnie funkcja zwrotna rodzica otrzymuje zweryfikowany profil.

    onSubmit({ name, dob });
    

    To jest efekt wcześniejszej decyzji projektowej. Formularz nie wie ani go nie obchodzi, co się stanie dalej; po prostu informuje o dostępności prawidłowych danych, a rodzic decyduje, co zrobić. Ten sam komponent mógłby dziś dostarczać dane do generatora horoskopów, a jutro do ekranu ustawień profilu bez żadnych zmian.

    Połącz logikę z markupiem

    Element formularza łączy wysłanie danych z obsługą tego wydarzenia.

    <form onSubmit={handleSubmit}>
    

    To mówi Reactowi, aby wykonał funkcję handleSubmit za każdym razem, gdy formularz zostanie wysłany, czy to poprzez kliknięcie przycisku, czy naciśnięcie klawisza Enter w polu. Następnie przychodzi pole z imieniem.

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

    Jak kontrolowany element wejściowy pozostaje zsynchronizowany

    To jest kontrolowane pole wprowadzania danych: to stan React, a nie DOM, stanowi źródło prawdy dotyczące jego wartości. Za każdym razem, gdy użytkownik pisze, uruchamiany jest obsługiwacz zmian.

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

    Czyta nowy tekst z wydarzenia i przechowuje go w stanie. Pełny cykl wygląda tak:

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

    Ponieważ pole zawsze pokazuje to, co zawiera name, wartość, którą walidujesz, jest gwarantowanie tą samą co ta widoczna na ekranie. Pole daty używa tego samego wzorca.

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

    Stan śledzi wybraną datę, a każda zmiana wywołuje setDob. Warto rozważyć dodanie natywnego atrybutu max ustawionego na dzisiejszą datę, co uniemożliwia większości wybieraków dat pokazywanie dni przyszłych, podczas gdy twój walidator nadal chroni przed wprowadzaniem nieprawidłowych danych i starszymi przeglądarkami.

    Każde pole wejściowe powinno mieć również widoczny <label> przypisany do niego. Zastępnik lub poblisky nagłówek nie mogą zastąpić etykiety; to właśnie etykieta jest ogłaszana przez czytniki ekranu i sprawia, że pole staje się klikalne dzięki swojemu opisowi.

    Pokazywać błędy tylko wtedy, gdy istnieją

    Wiadomość o błędzie powinna pojawiać się tylko wtedy, gdy taki istnieje. Obsługuje to warunkowe renderowanie.

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

    Gdy error zawiera tekst, akapit jest renderowany; gdy jest to pusta strona, która jest wartością fałszywą, nic się nie pojawia. Ten skrót && jest tutaj bezpieczny, ponieważ wartość to strona. W przypadku liczb może to dać błędny wynik: liczba 0 zostałaby wyświetlona jako dosłowne „0”.

    Akapit ten ma również rolę ARIA.

    role="alert"
    

    role="alert" informuje technologie wspomagające, że ten treść jest ważna i wymaga natychmiastowej uwagi, dzięki czemu czytniki ekranu ogłaszają ją zaraz po jej pojawieniu się. Jest to zmiana jednego atrybutu, która umożliwia korzystanie z informacji zwrotnych dotyczących walidacji przez osoby, które nie mogą zobaczyć pojawiającej się wiadomości. Dla większej przejrzystości można również oznaczyć problematyczne pole atrybutem aria-invalid i powiązać je z wiadomością za pomocą atrybutu aria-describedby.

    Dodaj przycisk wysyłania

    Ostatnim elementem jest przycisk wyraźnie zadeklarowany jako przycisk wysyłania.

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

    Wewnątrz formularza przycisk o atrybucie type="submit" uruchamia metodę onSubmit formularza, a wraz z nią również handleSubmit. Przyciski w formularzu domyślnie służą do wysłania danych, ale określenie tego typu zapobiega niespodziankom, gdy ktoś później dodaje drugi przycisk przeznaczony do czegoś innego, na przykład do usunięcia danych z pól.

    Pełny przepływ danych

    Patrząc na to całościowo, komponent przekazuje dane w jednym kierunku:

    State
      ↓
    User input
      ↓
    Submit
      ↓
    Validation
      ↓
    Parent callback
    
    • useState przechowuje to, co wpisał użytkownik.
    • Pola wejściowe aktualizują ten stan przy każdej zmianie.
    • Wysłanie formularza uruchamia metodę handleSubmit.
    • handleSubmit weryfikuje wprowadzone wartości.
    • Nieważne dane ustawiają stan błędu i proces zostaje zatrzymany.
    • Ważne dane trafiają do komponentu nadrzędnego przez metodę onSubmit, a ten je stamtąd odbiera.

    Dalej co robić

    To ręcznie opracowane podejście jest idealne do nauki i w pełni wystarczające dla formy z dwoma polami. Gdy forma zawiera wiele pól lub wymaga reguł między polami oraz weryfikacji po stronie serwera, rozważ użycie biblioteki schematów, aby te same reguły działały zarówno po stronie klienta, jak i serwera; jednym ze sposobów jest udostępnianie tego samego schematu Zod pomiędzy interfejsem React a backendem Node. Walidacja po stronie klienta poprawia doświadczenie użytkownika, ale nigdy nie zastępuje walidacji na serwerze, ponieważ każde żądanie może zostać sfałszowane ręcznie.

    Główne wnioski

    • Zaznacz tylko komponenty interaktywne tagiem "use client" i utrzymuj je w małych rozmiarach.
    • Nadaj formie jedno zadanie: zbieranie danych, ich walidacja i przekazanie dalej. Pozwól komponentowi nadrzędnemu zarządzać efektami ubocznymi za pomocą typizowanego callbacka.
  • Używaj kontrolowanych pól wprowadzania danych inicjalizowanych ciągami znaków, aby wartość wyświetlana na ekranie była tą samą, którą walidujesz.
  • Umieszczaj reguły walidacji w czystych funkcjach, które zwracają komunikat lub null; są one łatwe do testowania i ponownego użycia.
  • Bądź ostrożny przy pracach z datami: normalizuj godzinę dnia lub porównuj ciągi znaków w formacie YYYY-MM-DD, aby uniknąć błędów związanych z różnicami stref czasowych.
  • Wykorzystuj wczesne zwracania, aby funkcja obsługująca wysłanie danych była prosta, a atrybut role="alert" w połączeniu z odpowiednimi etykietami zapewniał dostępność informacji o błędach.
  • Zdolność do wyjaśnienia, dlaczego istnieje każdy element kodu, szczególnie te zaproponowane przez asystenta AI, jest częścią ukończenia pracy.
  • Literatura pokrewna

  • Zastępowanie as-Casts analizą Zod na każdej graniczce danych Next.js — Dlaczego konwersja typów w TypeScript nie chroni przed zmianami w API oraz jak schemat Zod waliduje wyniki fetch, formy, obsługę tras i działania serwera w Next.js.
  • Reakcyjne akordony dostępne do indeksowania: składanie za pomocą CSS Grid zamiast unmounting — Tworzenie nawarstwionych akordonów w React, które zachowują treść w DOM dla celów indeksowania, animowanie wysokości za pomocą wierszy grid, ograniczenie jednego otwartego elementu na poziomie oraz przywracanie stanu po zamknięciu.