Lekcja 7: Obsługa Zdarzeń (Event Handling)

Aplikacje React są interaktywne, co oznacza, że muszą reagować na działania użytkownika, takie jak kliknięcia myszą, wpisywanie tekstu, najechanie kursorem itp. Obsługa tych zdarzeń w React jest bardzo podobna do obsługi zdarzeń w standardowym HTML DOM, ale z kilkoma istotnymi różnicami składniowymi i koncepcyjnymi.

Podstawy Obsługi Zdarzeń w React

Przykłady Obsługi Zdarzeń

Obsługa Kliknięcia (onClick)

import React, { useState } from \'react\';

function ClickButton() {
  const [message, setMessage] = useState(\'\');

  // Definicja funkcji obsługi zdarzenia
  const handleClick = () => {
    setMessage(\'Przycisk został kliknięty!\');
    console.log(\'Kliknięto!\');
  };

  return (
    <div>
      {/* Przekazanie referencji do funkcji jako handlera onClick */}
      <button onClick={handleClick}>Kliknij mnie</button>
      <p>{message}</p>
    </div>
  );
}

export default ClickButton;

Możemy również zdefiniować funkcję inline za pomocą funkcji strzałkowej:

function ClickButtonInline() {
  return (
    <button onClick={() => console.log(\'Kliknięto inline!\')}>
      Kliknij mnie (inline)
    </button>
  );
}

Obsługa Zmiany Wartości (onChange) w Polach Formularza

Zdarzenie onChange jest kluczowe przy pracy z kontrolowanymi komponentami formularzy (o których więcej w lekcji o formularzach). Pozwala reagować na zmiany wprowadzane przez użytkownika w polach input, textarea, select.

import React, { useState } from \'react\';

function InputExample() {
  const [text, setText] = useState(\'\');

  // Handler otrzymuje obiekt zdarzenia \'e\'
  const handleChange = (e) => {
    // Dostęp do wartości pola przez e.target.value
    setText(e.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={handleChange} placeholder="Wpisz coś..." />
      <p>Wpisano: {text}</p>
    </div>
  );
}

export default InputExample;

Obsługa Wysłania Formularza (onSubmit)

Aby zapobiec domyślnemu przeładowaniu strony przy wysyłaniu formularza, używamy e.preventDefault().

import React, { useState } from \'react\';

function SimpleForm() {
  const [inputValue, setInputValue] = useState(\'\');

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault(); // Zapobiegaj domyślnemu przeładowaniu strony
    alert(`Wysłano wartość: ${inputValue}`);
    setInputValue(\'\'); // Wyczyść pole po wysłaniu
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" value={inputValue} onChange={handleChange} />
      <button type="submit">Wyślij</button>
    </form>
  );
}

export default SimpleForm;

Przekazywanie Argumentów do Handlerów Zdarzeń

Czasami chcemy przekazać dodatkowe argumenty do funkcji obsługi zdarzenia, np. identyfikator elementu, który został kliknięty.

Metoda 1: Funkcja Strzałkowa Inline

Możemy użyć funkcji strzałkowej bezpośrednio w JSX, która wywoła nasz handler z dodatkowymi argumentami.

function ButtonList() {
  const handleButtonClick = (id) => {
    console.log(`Kliknięto przycisk o ID: ${id}`);
  };

  return (
    <div>
      <button onClick={() => handleButtonClick(1)}>Przycisk 1</button>
      <button onClick={() => handleButtonClick(2)}>Przycisk 2</button>
    </div>
  );
}

Uwaga: Tworzenie funkcji inline przy każdym renderowaniu może mieć niewielki wpływ na wydajność w bardzo złożonych komponentach lub listach. W większości przypadków jest to jednak akceptowalne.

Metoda 2: Metoda `bind` (mniej popularna w Hookach)

W komponentach klasowych często używano this.handleClick.bind(this, arg1). W komponentach funkcyjnych jest to mniej powszechne, preferuje się funkcje strzałkowe.

Metoda 3: Atrybuty `data-*`

Możemy przechowywać dane w atrybutach `data-*` i odczytywać je z `e.target.dataset`.

function ButtonDataList() {
  const handleButtonClick = (e) => {
    const id = e.target.dataset.id;
    console.log(`Kliknięto przycisk o ID: ${id}`);
  };

  return (
    <div>
      <button data-id="1" onClick={handleButtonClick}>Przycisk 1</button>
      <button data-id="2" onClick={handleButtonClick}>Przycisk 2</button>
    </div>
  );
}

Syntetyczny Obiekt Zdarzenia (SyntheticEvent)

Jak wspomniano, React dostarcza obiekt `SyntheticEvent`, który normalizuje zachowanie zdarzeń między różnymi przeglądarkami. Ma on ten sam interfejs co natywny obiekt zdarzenia przeglądarki, w tym metody `stopPropagation()` i `preventDefault()`, ale działa identycznie we wszystkich przeglądarkach.

Jeśli z jakiegoś powodu potrzebujesz dostępu do natywnego obiektu zdarzenia, możesz go uzyskać przez właściwość nativeEvent: e.nativeEvent.

Ważne: Obiekty `SyntheticEvent` są reużywane ze względów wydajnościowych. Oznacza to, że po zakończeniu działania handlera zdarzenia, właściwości obiektu są zerowane. Jeśli potrzebujesz dostępu do właściwości zdarzenia w sposób asynchroniczny (np. w `setTimeout`), musisz albo zapisać potrzebne właściwości do zmiennej, albo wywołać `e.persist()` (choć ta metoda jest obecnie mniej zalecana na rzecz zapisywania wartości).


Ćwiczenie praktyczne

Pokaż rozwiązanie
// src/ToggleButton.jsx
import React, { useState } from \'react\';

function ToggleButton() {
  const [isOn, setIsOn] = useState(false);

  const handleClick = () => {
    setIsOn(prevIsOn => !prevIsOn);
  };

  return (
    <div>
      <button onClick={handleClick}>
        {isOn ? \'Wyłącz\' : \'Włącz\'}
      </button>
      <p>Stan: {isOn ? \'Włączony\' : \'Wyłączony\'}</p>
    </div>
  );
}

export default ToggleButton;

// W App.jsx
import React from \'react\';
import ToggleButton from \'./ToggleButton\";

function App() {
  return (
    <div>
      <h1>Przełącznik</h1>
      <ToggleButton />
    </div>
  );
}

export default App;

Cel: Stworzyć prosty przełącznik typu "Włącz/Wyłącz".

Kroki:

  1. Stwórz komponent funkcyjny ToggleButton.
  2. Użyj useState do przechowywania stanu przełącznika (np. isOn, boolean, początkowo false).
  3. Wyrenderuj przycisk. Tekst na przycisku powinien zależeć od stanu isOn (np. "Włącz" gdy false, "Wyłącz" gdy true).
  4. Dodaj handler onClick do przycisku.
  5. W handlerze onClick, przełączaj stan isOn na przeciwną wartość (użyj formy funkcyjnej setIsOn(prevIsOn => !prevIsOn)).
  6. Opcjonalnie: Dodaj paragraf poniżej przycisku, który wyświetla tekst "Stan: Włączony" lub "Stan: Wyłączony" w zależności od stanu isOn.
  7. Użyj komponentu ToggleButton w App.jsx.

Zadanie do samodzielnego wykonania

Stwórz komponent "Mouse Tracker", który:

  1. Wyświetla aktualne współrzędne kursora myszy (X, Y) wewnątrz diva.
  2. Używa useState do przechowywania obiektu ze współrzędnymi { x: 0, y: 0 }.
  3. Używa useEffect do dodania nasłuchiwania zdarzenia mousemove na całym oknie (window) po zamontowaniu komponentu.
  4. W funkcji obsługi zdarzenia mousemove aktualizuje stan współrzędnych na podstawie e.clientX i e.clientY.
  5. Implementuje funkcję czyszczącą w useEffect, która usuwa nasłuchiwanie zdarzenia mousemove przy odmontowywaniu komponentu.
  6. Wyświetla współrzędne wewnątrz jakiegoś elementu, np. <p>X: {coords.x}, Y: {coords.y}</p>.

FAQ - Obsługa Zdarzeń (Event Handling)

Jaka jest różnica między `onClick` w React a `onclick` w HTML?

Główne różnice to: 1) Nazewnictwo: `onClick` (camelCase) w React vs `onclick` (lowercase) w HTML. 2) Wartość: W React przekazujemy funkcję (`onClick={handleClick}`), a w HTML często string z kodem (`onclick="handleClick()"`) lub bezpośrednio kod JS. 3) Obiekt zdarzenia: React używa `SyntheticEvent` dla spójności między przeglądarkami.

Czy mogę używać `addEventListener` bezpośrednio w komponencie React?

Można, ale zazwyczaj nie jest to zalecane dla zdarzeń na elementach renderowanych przez React. Lepiej używać wbudowanych propsów zdarzeń (np. `onClick`). `addEventListener` jest potrzebny głównie do nasłuchiwania zdarzeń na obiektach globalnych (jak `window` czy `document`) lub elementach poza drzewem Reacta, i należy go używać wewnątrz `useEffect` z odpowiednim czyszczeniem.

Co to jest propagacja zdarzeń (event bubbling) w React?

Podobnie jak w DOM, zdarzenia w React propagują (bąbelkują) w górę drzewa komponentów. Oznacza to, że zdarzenie wywołane na elemencie wewnętrznym może być również obsłużone przez handlery przypisane do jego rodziców. Można zatrzymać propagację, wywołując `e.stopPropagation()` na obiekcie `SyntheticEvent`.

Jak obsłużyć zdarzenia `onMouseEnter` i `onMouseLeave`?

Obsługuje się je analogicznie do `onClick`, przekazując funkcje jako propsy `onMouseEnter` i `onMouseLeave` do elementu JSX, który ma na nie reagować. Te zdarzenia są przydatne do tworzenia efektów hover.

Czy przekazywanie funkcji strzałkowej inline w `onClick` jest złe dla wydajności?

Tworzenie nowej funkcji przy każdym renderowaniu (jak w `onClick={() => doSomething()}`) technicznie może wpłynąć na wydajność, ponieważ funkcja przekazywana jako props będzie inna przy każdym renderowaniu, co może powodować niepotrzebne ponowne renderowanie komponentu dziecka (jeśli używa `React.memo` lub `PureComponent`). W praktyce, dla większości aplikacji ten wpływ jest pomijalny. Optymalizacje (np. `useCallback`) są potrzebne tylko w specyficznych przypadkach.

Jak przekazać sam obiekt zdarzenia do handlera, gdy używam funkcji strzałkowej inline z argumentami?

Obiekt zdarzenia jest automatycznie przekazywany jako pierwszy argument do funkcji inline. Możesz go jawnie przekazać dalej do swojego handlera: `onClick={(e) => handleClick(e, customArg)}`.

Czy wszystkie zdarzenia DOM są dostępne w React?

React opakowuje większość popularnych zdarzeń DOM. Pełną listę obsługiwanych zdarzeń można znaleźć w dokumentacji React. Jeśli potrzebujesz obsłużyć bardzo rzadkie zdarzenie, które nie ma odpowiednika w `SyntheticEvent`, możesz użyć referencji (ref) do elementu DOM i dodać nasłuchiwanie za pomocą `addEventListener` w `useEffect`.