Lekcja 15: Custom Hooks (Niestandardowe Hooki)

Jedną z najpotężniejszych funkcji Hooków jest możliwość tworzenia własnych, niestandardowych Hooków (custom hooks). Pozwalają one na ekstrakcję i reużycie logiki stanowej oraz efektów ubocznych z komponentów funkcyjnych, czyniąc kod bardziej modularnym, czytelnym i łatwiejszym do testowania.

Czym są Niestandardowe Hooki?

Niestandardowy Hook to po prostu funkcja JavaScript, której nazwa zaczyna się od słowa "use" (np. useFetch, useFormInput, useWindowWidth) i która może wywoływać inne Hooki (np. useState, useEffect, useContext lub inne niestandardowe Hooki).

Nie są one częścią API Reacta, lecz wzorcem projektowym umożliwionym przez Hooki. Pozwalają na enkapsulację złożonej logiki, która może być następnie łatwo współdzielona między różnymi komponentami funkcyjnymi bez potrzeby stosowania bardziej skomplikowanych wzorców jak HOC (Higher-Order Components) czy Render Props.

Motywacja: Reużycie Logiki Stanowej

Wyobraźmy sobie, że w wielu miejscach aplikacji potrzebujemy śledzić szerokość okna przeglądarki. W lekcji o useEffect stworzyliśmy komponent WindowWidthLogger, który zawierał logikę useState i useEffect do tego celu. Jeśli potrzebujemy tej samej funkcjonalności w innym komponencie, musielibyśmy skopiować i wkleić tę logikę. Niestandardowe Hooki pozwalają uniknąć tej redundancji.

Tworzenie Niestandardowego Hooka

Stwórzmy niestandardowy Hook useWindowWidth, który enkapsuluje logikę śledzenia szerokości okna:

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

// Niestandardowy Hook - funkcja zaczynająca się od "use"
function useWindowWidth() {
  // Wewnątrz niestandardowego Hooka możemy używać innych Hooków
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };

    // Dodaj nasłuchiwacz
    window.addEventListener(\'resize\', handleResize);
    console.log(\'Dodano nasłuchiwacz resize (useWindowWidth)\');

    // Funkcja czyszcząca - usuń nasłuchiwacz
    return () => {
      window.removeEventListener(\'resize\', handleResize);
      console.log(\'Usunięto nasłuchiwacz resize (useWindowWidth)\');
    };
  }, []); // Pusta tablica - uruchom tylko raz

  // Zwróć wartość, której potrzebują komponenty
  return width;
}

export default useWindowWidth;

Kluczowe elementy:

Użycie Niestandardowego Hooka

Teraz możemy użyć naszego Hooka useWindowWidth w dowolnym komponencie funkcyjnym, tak jakby był to wbudowany Hook:

import React from \'react\';
import useWindowWidth from \'./useWindowWidth\'; // Zaimportuj niestandardowy Hook

function ResponsiveComponent() {
  // Użyj niestandardowego Hooka
  const windowWidth = useWindowWidth();

  return (
    <div>
      <p>Aktualna szerokość okna: {windowWidth}px</p>
      {windowWidth > 768 ? (
        <p>Wyświetlam wersję na duże ekrany.</p>
      ) : (
        <p>Wyświetlam wersję na małe ekrany.</p>
      )}
    </div>
  );
}

function AnotherComponent() {
    const width = useWindowWidth();
    // ... inna logika wykorzystująca szerokość okna
    return <p>Szerokość w innym komponencie: {width}px</p>;
}

// W App.jsx
function App() {
    return (
        <div>
            <ResponsiveComponent />
            <hr />
            <AnotherComponent />
        </div>
    );
}

export default App;

Ważne: Każde wywołanie niestandardowego Hooka tworzy niezależny stan i efekty. Jeśli ResponsiveComponent i AnotherComponent używają useWindowWidth, każdy z nich będzie miał własną zmienną stanu width i własny efekt nasłuchujący zdarzenia resize (choć w tym konkretnym przypadku oba będą reagować na to samo globalne zdarzenie).

Inne Przykłady Niestandardowych Hooków

useFetch - Do pobierania danych

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

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    setError(null);
    setData(null);

    const controller = new AbortController(); // Do anulowania zapytania
    const signal = controller.signal;

    fetch(url, { signal })
      .then(res => {
        if (!res.ok) throw new Error(\'Błąd sieci\');
        return res.json();
      })
      .then(fetchedData => {
        setData(fetchedData);
        setLoading(false);
      })
      .catch(err => {
        if (err.name !== \'AbortError\') { // Ignoruj błąd anulowania
          setError(err.message);
          setLoading(false);
        }
      });

    // Funkcja czyszcząca - anuluj zapytanie przy odmontowywaniu lub zmianie URL
    return () => {
      controller.abort();
    };
  }, [url]); // Zależność od URL

  return { data, loading, error };
}

// Użycie:
// function PostsList() {
//   const { data: posts, loading, error } = useFetch(\'https://jsonplaceholder.typicode.com/posts\');
//   if (loading) return 

Ładowanie...

; // if (error) return

Błąd: {error}

; // return
    {posts.map(post =>
  • {post.title}
  • )}
; // }

useFormInput - Do zarządzania pojedynczym polem formularza

import { useState } from \'react\';

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

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

  // Zwracamy obiekt z wartością i handlerem, gotowy do rozproszenia na inpucie
  return {
    value,
    onChange: handleChange
  };
}

// Użycie:
// function SimpleForm() {
//   const nameInput = useFormInput(\'\');
//   const emailInput = useFormInput(\'\');
//
//   const handleSubmit = (e) => {
//     e.preventDefault();
//     console.log(nameInput.value, emailInput.value);
//   }
//
//   return (
//     
// // // //
// ); // }

Zalety Niestandardowych Hooków


Ćwiczenie praktyczne

Pokaż rozwiązanie
// src/useToggle.js
import { useState, useCallback } from \'react\';

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  // Używamy useCallback, aby funkcja toggle nie tworzyła się na nowo przy każdym renderowaniu
  // (choć w tym prostym przypadku nie jest to krytyczne)
  const toggle = useCallback(() => {
    setValue(prevValue => !prevValue);
  }, []); // Pusta tablica zależności, bo toggle nie zależy od niczego z zewnątrz

  return [value, toggle];
}

export default useToggle;

// src/App.jsx
import React from \'react\';
import useToggle from \'./useToggle\';

function App() {
  const [isVisible, toggleVisibility] = useToggle(true);
  const [isDarkMode, toggleDarkMode] = useToggle(false);

  const appStyle = {
      background: isDarkMode ? \'#333\' : \'#FFF\',
      color: isDarkMode ? \'#FFF\' : \'#333\',
      padding: \'20px\',
      minHeight: \'200px\'
  };

  return (
    <div style={appStyle}>
      <h1>Niestandardowy Hook useToggle</h1>
      
      <div>
        <button onClick={toggleVisibility}>
          {isVisible ? \'Ukryj\' : \'Pokaż\'} tekst
        </button>
        {isVisible && <p>Ten tekst można ukryć lub pokazać.</p>}
      </div>

      <hr />

      <div>
        <button onClick={toggleDarkMode}>
          Przełącz tryb {isDarkMode ? \'jasny\' : \'ciemny\'}
        </button>
        <p>Aktualny tryb: {isDarkMode ? \'Ciemny\' : \'Jasny\'}</p>
      </div>
    </div>
  );
}

export default App;

Cel: Stworzyć niestandardowy Hook useToggle do zarządzania stanem typu boolean (włącz/wyłącz).

Kroki:

  1. Stwórz plik np. useToggle.js.
  2. Zdefiniuj funkcję useToggle, która przyjmuje opcjonalną wartość początkową (initialValue, domyślnie false).
  3. Wewnątrz Hooka użyj useState do przechowywania wartości boolean (np. value).
  4. Zdefiniuj funkcję toggle, która będzie przełączać wartość stanu boolean na przeciwną (użyj formy funkcyjnej setValue(prev => !prev)).
  5. Zwróć z Hooka tablicę zawierającą aktualną wartość stanu i funkcję toggle (podobnie jak zwraca useState).
  6. W komponencie App.jsx (lub innym) zaimportuj i użyj Hooka useToggle do stworzenia prostego przełącznika (np. pokazującego/ukrywającego jakiś element).

Zadanie do samodzielnego wykonania

Stwórz niestandardowy Hook useLocalStorage, który pozwala na łatwe zarządzanie wartością w Local Storage przeglądarki.

  1. Hook powinien przyjmować klucz (key) i wartość początkową (initialValue).
  2. Powinien używać useState do przechowywania aktualnej wartości w komponencie. Wartość początkowa stanu powinna być odczytana z Local Storage (jeśli istnieje pod danym kluczem), w przeciwnym razie użyj initialValue. (Pamiętaj, że Local Storage przechowuje stringi, może być potrzebne JSON.parse i JSON.stringify).
  3. Hook powinien zwracać aktualną wartość i funkcję do jej ustawiania (podobnie jak useState).
  4. Funkcja ustawiająca wartość powinna nie tylko aktualizować stan w React, ale również zapisywać nową wartość do Local Storage pod podanym kluczem.
  5. Opcjonalnie: Użyj useEffect, aby nasłuchiwać zdarzenia storage na window, aby zaktualizować stan, jeśli wartość w Local Storage zostanie zmieniona w innej karcie przeglądarki.
  6. Przetestuj Hooka w komponencie, np. do zapisywania preferencji użytkownika (jak imię czy motyw).

FAQ - Custom Hooks (Niestandardowe Hooki)

Czy niestandardowe Hooki współdzielą stan między komponentami?

Nie, każde wywołanie niestandardowego Hooka w komponencie tworzy jego własną, niezależną instancję stanu i efektów. Jeśli chcesz współdzielić *ten sam* stan między komponentami, musisz użyć podnoszenia stanu (lifting state up), Context API lub biblioteki do zarządzania stanem globalnym.

Jaka jest różnica między niestandardowym Hookiem a zwykłą funkcją pomocniczą?

Niestandardowe Hooki mogą wywoływać inne Hooki (np. `useState`, `useEffect`), podczas gdy zwykłe funkcje JavaScript nie mogą. Nazwa niestandardowego Hooka musi zaczynać się od "use". Hooki są przeznaczone do enkapsulacji logiki *stanowej* i *efektów ubocznych* związanych z Reactem.

Czy mogę używać niestandardowych Hooków w komponentach klasowych?

Nie, podobnie jak wbudowane Hooki, niestandardowe Hooki mogą być używane tylko w komponentach funkcyjnych lub innych niestandardowych Hookach.

Czy niestandardowe Hooki muszą coś zwracać?

Zazwyczaj tak. Niestandardowe Hooki są tworzone po to, aby dostarczyć komponentom pewne wartości (stan) lub funkcje (do modyfikacji stanu, wywołania akcji). To, co zwracają, zależy od ich przeznaczenia. Mogą zwracać pojedynczą wartość, tablicę (jak `useState`) lub obiekt.

Gdzie umieszczać pliki z niestandardowymi Hookami?

Dobrą praktyką jest umieszczanie każdego niestandardowego Hooka w osobnym pliku (np. `useFetch.js`, `useWindowWidth.js`) w dedykowanym folderze, np. `src/hooks/`. Ułatwia to organizację i importowanie Hooków w komponentach.

Czy istnieją gotowe biblioteki z niestandardowymi Hookami?

Tak, istnieje wiele popularnych bibliotek open-source, które dostarczają gotowe do użycia, dobrze przetestowane niestandardowe Hooki do różnych zadań, np. `react-use`, `usehooks-ts`. Korzystanie z nich może przyspieszyć development i zapewnić dostęp do zoptymalizowanych rozwiązań typowych problemów.

Czy tworzenie własnych Hooków jest trudne?

Jeśli rozumiesz działanie podstawowych Hooków (`useState`, `useEffect`), tworzenie prostych niestandardowych Hooków jest stosunkowo łatwe. Polega głównie na wydzieleniu istniejącej logiki z komponentu do osobnej funkcji zaczynającej się od "use". Kluczem jest zrozumienie, jaki stan i jakie efekty są potrzebne oraz co Hook powinien zwracać.