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.
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:
- Pamiętać to, co wpisał użytkownik.
- Sprawdzać dane przed ich wysłaniem.
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:
useStatedo przechowywania aktualnych wartości,- obsługi zdarzeń
onChangew polach wprowadzania danych, - obsługi zdarzenia
onSubmitw formularzu, - ciągłej interakcji z użytkownikiem.
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:
- Zatrzymać standardowe działanie przeglądarki przy wysyłaniu danych, które spowodowałoby ponowne załadowanie lub przeglądanie strony,
- Potwierdzić, że oba pola zawierają wartości,
- Walidować datę urodzenia,
- Pokazać błąd, jeśli coś jest nie tak,
- 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
useStateprzechowuje to, co wpisał użytkownik.- Pola wejściowe aktualizują ten stan przy każdej zmianie.
- Wysłanie formularza uruchamia metodę
handleSubmit. handleSubmitweryfikuje 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.
null; są one łatwe do testowania i ponownego użycia.YYYY-MM-DD, aby uniknąć błędów związanych z różnicami stref czasowych.role="alert" w połączeniu z odpowiednimi etykietami zapewniał dostępność informacji o błędach.Literatura pokrewna
- Ręcznie budowane formularze React: kontrolowane pola, walidacja i stany zapisu — Dowiedz się, jak działają formularze React, od kontrolowanych pól i obsługiwaczy według typu po walidację, stany zapisu, pola dynamiczne, przesyłanie plików oraz kiedy biblioteki formularzy są przydatne.
- Budowanie odpornej strony z detalami filmu za pomocą Next.js App Router — Naucz się, jak poprawnie pobierać i przechowywać dane z API OMDB w Next.js App Router przy użyciu asynchronicznych komponentów serwerowych, parametrów oczekiwanych oraz prawidłowej obsługi błędu 404.