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:

  1. Przechowywanie wartości pola formularza w stanie komponentu React (za pomocą useState).
  2. Ustawienie wartości pola formularza za pomocą atrybutu value (lub checked dla checkboxów/radio).
  3. 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:

  1. Stwórz komponent funkcyjny ContactForm.
  2. Użyj useState do przechowywania stanu dla każdego pola: name, email, message (wszystkie początkowo puste stringi).
  3. 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>).
  4. Powiąż wartość każdego pola z odpowiednim stanem za pomocą atrybutu value.
  5. Dodaj handler onChange do każdego pola, który aktualizuje odpowiedni stan.
  6. Dodaj przycisk "Wyślij" (<button type="submit">).
  7. Dodaj handler onSubmit do formularza, który wywołuje event.preventDefault() i wyświetla wprowadzone dane (np. w alert lub console.log).
  8. Użyj komponentu ContactForm w App.jsx.

Zadanie do samodzielnego wykonania

Zmodyfikuj formularz kontaktowy z ćwiczenia praktycznego:

  1. Zamiast trzech osobnych stanów, użyj jednego stanu będącego obiektem, np. formData z kluczami name, email, message.
  2. Stwórz jeden generyczny handler handleChange, który będzie aktualizował odpowiednie pole w obiekcie formData na podstawie atrybutu name pola input/textarea.
  3. Dodaj pole wyboru (<select>) dla tematu wiadomości (np. "Zapytanie", "Opinia", "Problem techniczny") i dodaj je do stanu formData 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.