Lekcja 10: Formularze (Forms)
Formularze są nieodłącznym elementem interaktywnych aplikacji webowych, pozwalając użytkownikom na wprowadzanie danych. W React obsługa formularzy różni się nieco od tradycyjnego podejścia w HTML DOM, głównie ze względu na koncepcję komponentów kontrolowanych (controlled components).
Komponenty Kontrolowane
W HTML elementy formularzy takie jak <input>
, <textarea>
i <select>
domyślnie same zarządzają swoim stanem i aktualizują go na podstawie danych wprowadzanych przez użytkownika. W React preferowanym podejściem jest uczynienie komponentu React "jedynym źródłem prawdy" (single source of truth) dla wartości pola formularza. Osiągamy to poprzez:
- Przechowywanie wartości pola formularza w stanie komponentu React (za pomocą
useState
). - Ustawienie wartości pola formularza za pomocą atrybutu
value
(lubchecked
dla checkboxów/radio). - Aktualizowanie stanu komponentu React za każdym razem, gdy użytkownik wprowadza zmianę, za pomocą handlera zdarzenia
onChange
.
Taki element formularza, którego wartość jest kontrolowana przez stan React, nazywamy komponentem kontrolowanym.
Przykład: Kontrolowany Input Tekstowy
import React, { useState } from \'react\';
function NameForm() {
// 1. Stan przechowuje wartość pola
const [name, setName] = useState(\'\');
// 3. Handler aktualizuje stan przy każdej zmianie
const handleChange = (event) => {
setName(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
alert(\'Podano imię: \' + name);
};
return (
<form onSubmit={handleSubmit}>
<label>
Imię:
{/* 2. Wartość pola jest powiązana ze stanem */}
<input type="text" value={name} onChange={handleChange} />
</label>
<button type="submit">Wyślij</button>
</form>
);
}
export default NameForm;
Dzięki temu podejściu, wartość pola input zawsze odzwierciedla stan komponentu React, a każda zmiana wartości musi przejść przez handler onChange
i aktualizację stanu.
Inne Elementy Formularzy
Textarea
W HTML wartość <textarea>
jest ustawiana przez jej zawartość (dzieci). W React, podobnie jak w przypadku inputów, używamy atrybutu value
.
import React, { useState } from \'react\';
function EssayForm() {
const [text, setText] = useState(\'Proszę napisać esej o swoim ulubionym elemencie DOM.\');
const handleChange = (event) => {
setText(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
alert(\'Wysłano esej: \' + text);
};
return (
<form onSubmit={handleSubmit}>
<label>
Esej:
<textarea value={text} onChange={handleChange} rows="5" cols="30" />
</label>
<button type="submit">Wyślij</button>
</form>
);
}
export default EssayForm;
Select (Dropdown)
W React, zamiast używać atrybutu selected
na elementach <option>
, ustawiamy atrybut value
na tagu <select>
. Stan przechowuje wartość wybranej opcji.
import React, { useState } from \'react\';
function FlavorForm() {
const [flavor, setFlavor] = useState(\'kokosowy\'); // Wartość domyślna
const handleChange = (event) => {
setFlavor(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
alert(\'Wybrano ulubiony smak: \' + flavor);
};
return (
<form onSubmit={handleSubmit}>
<label>
Wybierz ulubiony smak lodów:
<select value={flavor} onChange={handleChange}>
<option value="grejpfrutowy">Grejpfrutowy</option>
<option value="limonkowy">Limonkowy</option>
<option value="kokosowy">Kokosowy</option>
<option value="mango">Mango</option>
</select>
</label>
<button type="submit">Wyślij</button>
</form>
);
}
export default FlavorForm;
Dla <select multiple={true}>
, atrybut value
przyjmuje tablicę wartości, a stan również powinien być tablicą.
Checkbox i Radio
Dla checkboxów i radio buttonów używamy atrybutu checked
zamiast value
do kontrolowania stanu, a onChange
do jego aktualizacji. Wartość odczytujemy z event.target.checked
.
import React, { useState } from \'react\';
function CheckboxExample() {
const [isChecked, setIsChecked] = useState(false);
const handleChange = (event) => {
setIsChecked(event.target.checked);
};
return (
<label>
Zgoda marketingowa:
<input
type="checkbox"
checked={isChecked}
onChange={handleChange}
/>
{isChecked ? \' (Zaznaczono)\' : \' (Nie zaznaczono)\'}
</label>
);
}
export default CheckboxExample;
Obsługa Wielu Pól Formularza
Gdy mamy formularz z wieloma polami, tworzenie osobnego handlera onChange
dla każdego pola może być uciążliwe. Możemy stworzyć jeden generyczny handler, wykorzystując atrybut name
pól formularza.
import React, { useState } from \'react\';
function ReservationForm() {
const [formData, setFormData] = useState({
isGoing: true,
numberOfGuests: 2
});
const handleInputChange = (event) => {
const target = event.target;
const value = target.type === \'checkbox\' ? target.checked : target.value;
const name = target.name; // Pobieramy \'name\' z elementu input
// Używamy obliczonej nazwy właściwości [name]
setFormData(prevData => ({
...prevData,
[name]: value
}));
}
const handleSubmit = (event) => {
event.preventDefault();
alert(`Idzie: ${formData.isGoing}, Gości: ${formData.numberOfGuests}`);
}
return (
<form onSubmit={handleSubmit}>
<label>
Czy idziesz?:
<input
name="isGoing" // Atrybut name
type="checkbox"
checked={formData.isGoing}
onChange={handleInputChange} />
</label>
<br />
<label>
Liczba gości:
<input
name="numberOfGuests" // Atrybut name
type="number"
value={formData.numberOfGuests}
onChange={handleInputChange} />
</label>
<button type="submit">Wyślij</button>
</form>
);
}
export default ReservationForm;
W tym podejściu, stan przechowuje obiekt z wartościami wszystkich pól, a handler handleInputChange
dynamicznie aktualizuje odpowiednią właściwość tego obiektu na podstawie atrybutu name
zmienianego pola.
Komponenty Niekontrolowane (Uncontrolled Components)
Alternatywą dla komponentów kontrolowanych są komponenty niekontrolowane, gdzie dane formularza są obsługiwane przez sam DOM, a nie przez stan React. Aby uzyskać dostęp do wartości pola, używamy referencji (refs).
Komponenty niekontrolowane są czasem prostsze w implementacji dla prostych formularzy lub przy integracji z kodem non-React, ale generalnie zaleca się stosowanie komponentów kontrolowanych, ponieważ ułatwiają one walidację, formatowanie danych i zarządzanie stanem formularza w sposób spójny z filozofią Reacta.
Ćwiczenie praktyczne
Pokaż rozwiązanie
// src/ContactForm.jsx
import React, { useState } from \'react\';
function ContactForm() {
const [name, setName] = useState(\'\');
const [email, setEmail] = useState(\'\');
const [message, setMessage] = useState(\'\');
const handleSubmit = (event) => {
event.preventDefault();
console.log(\'Wysłano formularz:\', { name, email, message });
alert(`Dziękujemy za wiadomość, ${name}!`);
// Opcjonalnie: wyczyść formularz po wysłaniu
setName(\'\');
setEmail(\'\');
setMessage(\'\');
};
return (
<form onSubmit={handleSubmit} style={{ display: \'flex\', flexDirection: \'column\', maxWidth: \'300px\', gap: \'10px\' }}>
<h2>Formularz Kontaktowy</h2>
<label>
Imię:
<input type="text" value={name} onChange={(e) => setName(e.target.value)} required />
</label>
<label>
Email:
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</label>
<label>
Wiadomość:
<textarea value={message} onChange={(e) => setMessage(e.target.value)} required />
</label>
<button type="submit">Wyślij</button>
</form>
);
}
export default ContactForm;
// W App.jsx
import React from \'react\';
import ContactForm from \'./ContactForm\";
function App() {
return (
<div>
<ContactForm />
</div>
);
}
export default App;
Cel: Stworzyć prosty formularz kontaktowy z polem na imię, email i wiadomość (textarea), używając komponentów kontrolowanych.
Kroki:
- Stwórz komponent funkcyjny
ContactForm
. - Użyj
useState
do przechowywania stanu dla każdego pola:name
,email
,message
(wszystkie początkowo puste stringi). - Wyrenderuj formularz (
<form>
) z trzema polami:<input type="text">
dla imienia,<input type="email">
dla emaila i<textarea>
dla wiadomości. Dodaj odpowiednie etykiety (<label>
). - Powiąż wartość każdego pola z odpowiednim stanem za pomocą atrybutu
value
. - Dodaj handler
onChange
do każdego pola, który aktualizuje odpowiedni stan. - Dodaj przycisk "Wyślij" (
<button type="submit">
). - Dodaj handler
onSubmit
do formularza, który wywołujeevent.preventDefault()
i wyświetla wprowadzone dane (np. walert
lubconsole.log
). - Użyj komponentu
ContactForm
wApp.jsx
.
Zadanie do samodzielnego wykonania
Zmodyfikuj formularz kontaktowy z ćwiczenia praktycznego:
- Zamiast trzech osobnych stanów, użyj jednego stanu będącego obiektem, np.
formData
z kluczaminame
,email
,message
. - Stwórz jeden generyczny handler
handleChange
, który będzie aktualizował odpowiednie pole w obiekcieformData
na podstawie atrybutuname
pola input/textarea. - Dodaj pole wyboru (
<select>
) dla tematu wiadomości (np. "Zapytanie", "Opinia", "Problem techniczny") i dodaj je do stanuformData
oraz obsłuż w generycznym handlerze.
FAQ - Formularze (Forms)
Dlaczego preferuje się komponenty kontrolowane?
Komponenty kontrolowane sprawiają, że stan komponentu React jest jedynym źródłem prawdy dla wartości formularza. Ułatwia to implementację walidacji na bieżąco, formatowanie wprowadzanych danych, warunkowe wyłączanie przycisku wysyłania oraz ogólne zarządzanie stanem formularza w sposób spójny z Reactem.
Kiedy warto rozważyć komponenty niekontrolowane?
Komponenty niekontrolowane mogą być prostsze w przypadku bardzo prostych formularzy, gdzie nie potrzebujemy walidacji na bieżąco ani skomplikowanego zarządzania stanem. Mogą być też użyteczne przy integracji z bibliotekami non-React lub przy obsłudze pól typu `<input type="file">`, których wartość jest tylko do odczytu.
Jak obsłużyć pole `<input type="file">`?
Pole `<input type="file">` jest zawsze komponentem niekontrolowanym w React, ponieważ jego wartość może być ustawiona tylko przez użytkownika ze względów bezpieczeństwa. Aby uzyskać dostęp do wybranych plików, używamy referencji (ref) do elementu input i odczytujemy właściwość `files` (np. `ref.current.files`).
Jak zwalidować formularz w React?
Walidację można przeprowadzać na bieżąco w handlerze `onChange` (aktualizując stan błędów obok stanu wartości) lub przy wysyłaniu formularza w handlerze `onSubmit`. Można pisać własną logikę walidacji lub skorzystać z popularnych bibliotek do obsługi formularzy, takich jak Formik, React Hook Form, które oferują zaawansowane funkcje walidacji i zarządzania stanem formularza.
Co to jest debouncing/throttling w kontekście formularzy?
Debouncing i throttling to techniki optymalizacji używane do ograniczania częstotliwości wywoływania funkcji, np. handlera `onChange`. Debouncing powoduje, że funkcja jest wywoływana dopiero po pewnym czasie od ostatniego zdarzenia (np. po zakończeniu pisania przez użytkownika). Throttling zapewnia, że funkcja jest wywoływana co najwyżej raz na określony interwał czasu. Są przydatne, gdy `onChange` wywołuje kosztowne operacje (np. zapytania API).
Jak ustawić wartość początkową pola formularza?
W komponencie kontrolowanym wartość początkową ustawiamy, inicjalizując odpowiedni stan w `useState` wartością początkową: `const [name, setName] = useState(\'Wartość początkowa\');`. Ta wartość zostanie automatycznie przypisana do atrybutu `value` pola formularza przy pierwszym renderowaniu.
Czy muszę używać `event.preventDefault()` w `onSubmit`?
Tak, jeśli nie chcesz, aby przeglądarka wykonała domyślną akcję wysłania formularza, która zazwyczaj polega na przeładowaniu strony. W aplikacjach typu Single Page Application (SPA) budowanych w React, zazwyczaj chcemy obsłużyć wysłanie formularza za pomocą JavaScript (np. wysyłając dane do API) bez przeładowywania strony, dlatego `event.preventDefault()` jest niezbędne.