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:
- Nazwa zaczyna się od "use".
- Wywołuje wbudowane Hooki (
useState
,useEffect
). - Zwraca wartość (lub wartości), która będzie użyteczna dla komponentów (w tym przypadku aktualną szerokość okna).
- Przestrzega reguł Hooków (Hooki są wywoływane na najwyższym poziomie wewnątrz
useWindowWidth
).
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
- Reużywalność: Łatwe współdzielenie logiki stanowej i efektów między komponentami.
- Czytelność: Komponenty stają się prostsze, ponieważ złożona logika jest ukryta w Hooku.
- Separacja odpowiedzialności: Logika niezwiązana bezpośrednio z renderowaniem UI jest wydzielona.
- Testowalność: Niestandardowe Hooki można testować niezależnie od komponentów UI.
Ć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:
- Stwórz plik np.
useToggle.js
. - Zdefiniuj funkcję
useToggle
, która przyjmuje opcjonalną wartość początkową (initialValue
, domyślniefalse
). - Wewnątrz Hooka użyj
useState
do przechowywania wartości boolean (np.value
). - Zdefiniuj funkcję
toggle
, która będzie przełączać wartość stanu boolean na przeciwną (użyj formy funkcyjnejsetValue(prev => !prev)
). - Zwróć z Hooka tablicę zawierającą aktualną wartość stanu i funkcję
toggle
(podobnie jak zwracauseState
). - W komponencie
App.jsx
(lub innym) zaimportuj i użyj HookauseToggle
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.
- Hook powinien przyjmować klucz (
key
) i wartość początkową (initialValue
). - 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żyjinitialValue
. (Pamiętaj, że Local Storage przechowuje stringi, może być potrzebneJSON.parse
iJSON.stringify
). - Hook powinien zwracać aktualną wartość i funkcję do jej ustawiania (podobnie jak
useState
). - Funkcja ustawiająca wartość powinna nie tylko aktualizować stan w React, ale również zapisywać nową wartość do Local Storage pod podanym kluczem.
- Opcjonalnie: Użyj
useEffect
, aby nasłuchiwać zdarzeniastorage
nawindow
, aby zaktualizować stan, jeśli wartość w Local Storage zostanie zmieniona w innej karcie przeglądarki. - 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ć.