Kurs JavaScript i TypeScript · Moduł 8: Zdarzenia i DOM

Dobre praktyki przy obsłudze wydarzeń

4 min czytania ·

Kluczowe zasady dla efektywnego i bezpiecznego zarządzania wydarzeniami w aplikacjach JavaScript.

"Większość awarii w centrum kontroli nie bierze się z trudnych problemów," mówi Ray Arnold, przeglądając dziennik incydentów. "To drobiazgi: czujnik podpięty do urządzenia, którego nie ma, alarm, którego nikt nie wyłączył, blokada założona na wszelki wypadek." Ta lekcja zbiera takie drobiazgi w zasady, które warto mieć we krwi. Każdą poznałeś już w działaniu, teraz układamy je w jedną checklistę.

Zasady dobrego kodu

Pierwsza zasada chroni przed najczęstszym błędem początkujących. Zanim podepniesz listener, upewnij się, że element w ogóle istnieje.

// 1. Zawsze sprawdzaj, czy element istnieje
const button = document.getElementById('my-button');
if (button) {
  button.addEventListener('click', handleClick);
} else {
  console.warn('Przycisk o ID "my-button" nie został znaleziony');
}

Metoda getElementById() zwraca null, gdy na stronie nie ma elementu o podanym ID, na przykład przez literówkę albo skrypt uruchomiony przed wczytaniem HTML. Wywołanie addEventListener() na null rzuca TypeError i zatrzymuje resztę skryptu. Skrótem byłby zapis button?.addEventListener(...), ale ja wolę jawny warunek z ostrzeżeniem, bo cichy ?. ukrywa literówkę, zamiast ją zgłosić.

Druga zasada dotyczy elementów, które pojawiają się i znikają. Zamiast podpinać listener do każdego przycisku, słuchaj na ich wspólnym rodzicu.

// 2. Używaj delegacji dla dynamicznych elementów
const parent = document.getElementById('dynamic-content');
parent.addEventListener('click', (e) => {
  if (e.target.matches('.dynamic-button')) {
    handleDynamicButtonClick(e);
  }
});

Warunek e.target.matches('.dynamic-button') przepuszcza tylko kliknięcia w przyciski z tą klasą, więc nowe przyciski działają bez żadnych dodatkowych listenerów. Pamiętaj z lekcji o delegacji, że matches() sprawdza wyłącznie kliknięty element. Jeśli w przycisku siedzi ikona, bezpieczniej użyć closest().

Trzecia zasada to sprzątanie. Komponent, który podpina listener, powinien też umieć go zdjąć.

// 3. Pamiętaj o czyszczeniu event listenerów
class ParkComponent {
  constructor(element) {
    this.element = element;
    this.handleClick = this.handleClick.bind(this);
    this.init();
  }

  init() {
    this.element.addEventListener('click', this.handleClick);
  }

  handleClick(e) {
    console.log('Kliknięto komponent parku');
  }

  destroy() {
    this.element.removeEventListener('click', this.handleClick);
  }
}

Kluczowa jest linia z bind(this) w konstruktorze. Metoda bind() tworzy nową funkcję z na stałe przypiętym this, a my zapisujemy ją jako this.handleClick. Dzięki temu init() i destroy() używają tej samej referencji, więc usunięcie działa. Bez bind() wewnątrz handleClick() wartość this wskazywałaby element, a nie komponent.

Czwarta zasada dotyczy blokowania domyślnych zachowań przeglądarki.

// 4. Używaj preventDefault() świadomie
const form = document.getElementById('contact-form');
form.addEventListener('submit', (e) => {
  e.preventDefault(); // Zatrzymuje standardowe wysłanie formularza

  // Własna logika walidacji i wysyłania
  if (validateForm()) {
    submitFormAjax();
  }
});

Tutaj preventDefault() ma konkretny powód: zatrzymuje standardowe wysłanie i przeładowanie strony, bo formularz obsłuży nasz własny kod. Nie wywołuj go na wszelki wypadek. Na linku zablokuje nawigację, przy dotyku może zatrzymać przewijanie, a w listenerze pasywnym zostanie po prostu zignorowany.

Piąta zasada: rejestruj listenery jednym stylem. Właściwość onclick przechowuje tylko jedną funkcję, a addEventListener() dopisuje kolejnych słuchaczy.

// onclick przechowuje tylko jedną funkcję - druga nadpisuje pierwszą
alarmButton.onclick = () => console.log('Alarm w sektorze A');
alarmButton.onclick = () => console.log('Alarm w sektorze B');

// addEventListener dopisuje kolejnych słuchaczy
alarmButton.addEventListener('click', () => console.log('Powiadom ochronę'));
alarmButton.addEventListener('click', () => console.log('Zapisz w dzienniku'));

Co wypisze jedno kliknięcie? Trzy linie: "Alarm w sektorze B", "Powiadom ochronę" i "Zapisz w dzienniku". Komunikat o sektorze A przepadł bez śladu, bo drugie przypisanie go nadpisało. Dlatego w całym projekcie trzymaj się addEventListener(), który nikomu niczego nie odbiera.

Krótka checklista na koniec

Poza pięcioma zasadami z kodu warto pamiętać o kilku kolejnych:

  • Listenery, które planujesz usunąć, trzymaj w nazwanej funkcji lub zmiennej, bo funkcji bez referencji nie zdejmiesz przez removeEventListener()
  • Częste wydarzenia, jak scroll, resize czy input, ograniczaj przez throttling albo debouncing
  • Listenerom dotyku i kółka myszy, które nie wywołują preventDefault(), dodawaj opcję { passive: true }
  • Do klikalnych elementów używaj <button>, a nie <div>

Ostatni punkt łatwo przeoczyć. Przycisk dostaje od przeglądarki obsługę klawiatury za darmo: po wciśnięciu Enter albo spacji wysyła to samo wydarzenie click co mysz. Klikalny div wymagałby ręcznego dopisania tej obsługi, a zwykle ktoś o tym zapomina.

Moja rada: zanim oddasz kod, przejdź przez tę listę jak przez poranną inspekcję ogrodzeń. Zajmuje minutę, a oszczędza godziny szukania błędów. W piaskownicy poniżej zobaczysz dobre i złe wersje tych samych rozwiązań obok siebie. W następnej lekcji wejdziemy w zaawansowane techniki, zaczynając od gestów dotykowych.

Pamiętaj: w parku nie ma drobnych zaniedbań, a każdy nieobsłużony null może otworzyć bramę, której nikt nie pilnuje.

Kod do tej lekcji: index.html
<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Dobre Praktyki Event Handling</title>
    <style>
        body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; }
        .demo-box { background: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; margin: 20px 0; }
        .good { border-left: 4px solid #27ae60; }
        .bad { border-left: 4px solid #e74c3c; }
        button { padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer; margin: 5px; }
        code { background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 3px; }
    </style>
</head>
<body>
    <h1>Dobre Praktyki Event Handling</h1>

    <div class="demo-box good">
        <h3>Dobre: Sprawdzenie istnienia elementu</h3>
        <button id="good-btn">Kliknij mnie</button>
        <p>Kod używa bezpiecznego pattern <code>element?.addEventListener</code></p>
    </div>

    <div class="demo-box good">
        <h3>Dobre: Delegacja eventów</h3>
        <div id="dino-container">
            <button class="dino-btn" data-dino="trex">T-Rex</button>
            <button class="dino-btn" data-dino="velociraptor">Velociraptor</button>
        </div>
    </div>

    <div class="demo-box good">
        <h3>Dobre: Czyszczenie listenerów</h3>
        <button id="cleanup-test">Test Cleanup</button>
        <p id="cleanup-status">Status: Nieaktywny</p>
    </div>

    <script>
        console.log('Demonstracja dobrych praktyk');

        // DOBRA PRAKTYKA 1: Sprawdzanie istnienia
        const goodBtn = document.getElementById('good-btn');
        if (goodBtn) {
            goodBtn.addEventListener('click', () => {
                console.log('Element exists - safe click');
                alert('Bezpieczne kliknięcie!');
            });
        }

        // DOBRA PRAKTYKA 2: Event Delegation
        const dinoContainer = document.getElementById('dino-container');
        dinoContainer.addEventListener('click', (e) => {
            if (e.target.classList.contains('dino-btn')) {
                const dinoName = e.target.dataset.dino;
                console.log(`Delegated click: ${dinoName}`);
                alert(`Wybrałeś: ${dinoName}`);
            }
        });

        // DOBRA PRAKTYKA 3: Proper Cleanup
        class ComponentWithCleanup {
            constructor(element) {
                this.element = element;
                this.handleClick = this.handleClick.bind(this);
                this.init();
            }

            init() {
                this.element.addEventListener('click', this.handleClick);
                console.log('Component initialized');
            }

            handleClick() {
                console.log('Component clicked');
                document.getElementById('cleanup-status').textContent = 'Status: Aktywny';
            }

            destroy() {
                this.element.removeEventListener('click', this.handleClick);
                console.log('Component cleaned up properly');
                document.getElementById('cleanup-status').textContent = 'Status: Wyczyszczony';
            }
        }

        const cleanupBtn = document.getElementById('cleanup-test');
        const component = new ComponentWithCleanup(cleanupBtn);

        // Symulacja cleanup po 5 sekundach
        setTimeout(() => {
            component.destroy();
        }, 5000);

        // DOBRA PRAKTYKA 4: preventDefault świadomie
        document.addEventListener('keydown', (e) => {
            if (e.ctrlKey && e.key === 's') {
                e.preventDefault();
                console.log('Prevented default save, implemented custom logic');
                alert('Zapisano stan parku!');
            }
        });

        console.log('Wszystkie dobre praktyki zaimplementowane');
    </script>
</body>
</html>

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Dlaczego przed wywołaniem addEventListener warto sprawdzić, czy element istnieje?

  2. 2. Po co wywołujemy e.preventDefault() w handlerze zdarzenia submit formularza?

Zadania praktyczne w grze

  • Klikanie w kolejności

    Ułóż warunek delegacji wydarzeń sprawdzający kliknięty element:

Przydatne artykuły