W kosmicznej flocie React, jednym z najcenniejszych umiejętności inżyniera jest projektowanie modułów wielokrotnego użytku. Wyobraź sobie, że budujesz systemy nawigacyjne, komunikacyjne i diagnostyczne dla setek statków kosmicznych. Zamiast pisać każdy system od zera dla każdego statku, tworzysz uniwersalne moduły, które można podłączyć do dowolnego pojazdu. Dokładnie tak działają Custom Hooks w React.
W poprzedniej lekcji poznaliśmy podstawy custom hooków - useFetch, useInterval, useLocalStorage i useToggle. Teraz zagłębimy się w zaawansowane wzorce, reguły hooków i kompozycję, które uczynią Twoje hooki naprawdę profesjonalnymi.
Zanim zaczniemy tworzyć zaawansowane hooki, musimy znać dwie fundamentalne reguły, których złamanie powoduje nieprzewidywalne błędy:
Hooki muszą być wywoływane zawsze w tej samej kolejności przy każdym renderowaniu. Nigdy nie umieszczaj hooków wewnątrz warunków, pętli ani zagnieżdżonych funkcji:
1// ZLE - hook wewnatrz warunku
2function SpaceShip({ hasRadar }) {
3 if (hasRadar) {
4 const [signal, setSignal] = useState(null); // BLAD!
5 }
6}
7
8// DOBRZE - warunek wewnatrz hooka
9function SpaceShip({ hasRadar }) {
10 const [signal, setSignal] = useState(null);
11
12 useEffect(() => {
13 if (hasRadar) {
14 // logika radaru
15 }
16 }, [hasRadar]);
17}React śledzi hooki na podstawie ich kolejności wywołania. Jeśli warunek spowoduje pominięcie jednego hooka, wszystkie następne będą miały błędne wartości - jak gdyby przesunęły się wszystkie moduły w statku kosmicznym.
Nie możesz wywoływać hooków z zwykłych funkcji JavaScript. Tylko:
use)1// ZLE - zwykla funkcja
2function calculateTrajectory() {
3 const [speed] = useState(0); // BLAD!
4}
5
6// DOBRZE - custom hook
7function useTrajectory() {
8 const [speed, setSpeed] = useState(0);
9 return { speed, setSpeed };
10}Hook
useDebounce jest niezwykle przydatny do wyszukiwania w czasie rzeczywistym. Zamiast wysyłać zapytanie do API po każdym naciśnięciu klawisza, czekamy aż użytkownik skończy pisać:1function useDebounce(value, delay) {
2 const [debouncedValue, setDebouncedValue] = useState(value);
3
4 useEffect(() => {
5 const timer = setTimeout(() => {
6 setDebouncedValue(value);
7 }, delay);
8
9 return () => clearTimeout(timer);
10 }, [value, delay]);
11
12 return debouncedValue;
13}
14
15// Uzycie - wyszukiwanie planet
16function PlanetSearch() {
17 const [query, setQuery] = useState('');
18 const debouncedQuery = useDebounce(query, 500);
19
20 useEffect(() => {
21 if (debouncedQuery) {
22 // Zapytanie wysylane dopiero po 500ms przerwy w pisaniu
23 fetch(\`/api/planets?search=\${debouncedQuery}\`);
24 }
25 }, [debouncedQuery]);
26
27 return (
28 <input
29 value={query}
30 onChange={e => setQuery(e.target.value)}
31 placeholder="Szukaj planet..."
32 />
33 );
34}Timer jest czyszczony przy każdej zmianie wartości. Dopiero gdy użytkownik przestanie pisać na 500ms, wartość zostanie zaktualizowana i zapytanie API wysłane.
Ten prosty hook pozwala porównywać aktualną wartość z poprzednią:
1function usePrevious(value) {
2 const ref = useRef();
3
4 useEffect(() => {
5 ref.current = value;
6 }, [value]);
7
8 return ref.current;
9}
10
11// Uzycie - monitorowanie zmian predkosci
12function SpeedMonitor() {
13 const [speed, setSpeed] = useState(0);
14 const previousSpeed = usePrevious(speed);
15
16 const trend = speed > previousSpeed
17 ? 'Przyspieszanie'
18 : speed < previousSpeed
19 ? 'Hamowanie'
20 : 'Stala predkosc';
21
22 return <p>{trend}: {speed} km/s (bylo: {previousSpeed})</p>;
23}Tablice wymagają specyficznych operacji (dodawanie, usuwanie, filtrowanie). Custom hook zamyka te operacje w czytelny interfejs:
1function useArray(initialArray = []) {
2 const [array, setArray] = useState(initialArray);
3
4 const push = (element) => setArray(a => [...a, element]);
5 const removeByIndex = (index) =>
6 setArray(a => a.filter((_, i) => i !== index));
7 const update = (index, newElement) =>
8 setArray(a => a.map((item, i) => (i === index ? newElement : item)));
9 const clear = () => setArray([]);
10 const filter = (fn) => setArray(a => a.filter(fn));
11
12 return { array, set: setArray, push, removeByIndex, update, clear, filter };
13}
14
15// Uzycie - lista zaloganow
16function CrewManager() {
17 const crew = useArray(['Kapitan Nova', 'Nawigator Rex']);
18
19 return (
20 <div>
21 {crew.array.map((member, i) => (
22 <div key={i}>
23 {member}
24 <button onClick={() => crew.removeByIndex(i)}>Usun</button>
25 </div>
26 ))}
27 <button onClick={() => crew.push('Nowy czlonek')}>
28 Dodaj czlonka zalogi
29 </button>
30 <button onClick={crew.clear}>Zwolnij zaloge</button>
31 </div>
32 );
33}Prawdziwa siła custom hooków ujawnia się, gdy łączymy je ze sobą. Hooki mogą wywoływać inne hooki, tworząc złożone zachowania z prostych bloków:
1// Hook 1: Prosty hook stanu z walidacja
2function useValidatedInput(initialValue, validator) {
3 const [value, setValue] = useState(initialValue);
4 const [error, setError] = useState(null);
5
6 const handleChange = (newValue) => {
7 setValue(newValue);
8 const validationError = validator(newValue);
9 setError(validationError);
10 };
11
12 return { value, error, onChange: handleChange, isValid: !error };
13}
14
15// Hook 2: Kompozycja - formularz z wieloma polami
16function useSpaceshipForm() {
17 const name = useValidatedInput('', (v) =>
18 v.length < 3 ? 'Min. 3 znaki' : null
19 );
20 const fuel = useValidatedInput(100, (v) =>
21 v < 0 || v > 100 ? 'Zakres 0-100' : null
22 );
23
24 const isFormValid = name.isValid && fuel.isValid;
25
26 const reset = () => {
27 name.onChange('');
28 fuel.onChange(100);
29 };
30
31 return { name, fuel, isFormValid, reset };
32}
33
34// Uzycie
35function ShipRegistration() {
36 const form = useSpaceshipForm();
37
38 return (
39 <form>
40 <input
41 value={form.name.value}
42 onChange={e => form.name.onChange(e.target.value)}
43 />
44 {form.name.error && <span>{form.name.error}</span>}
45
46 <input
47 type="number"
48 value={form.fuel.value}
49 onChange={e => form.fuel.onChange(Number(e.target.value))}
50 />
51 {form.fuel.error && <span>{form.fuel.error}</span>}
52
53 <button disabled={!form.isFormValid}>Zarejestruj statek</button>
54 </form>
55 );
56}useSpaceshipForm używa useValidatedInput dwukrotnie - każde wywołanie tworzy niezależną instancję ze swoim stanem.Popularne wzorce custom hooków, które warto znać:
| Hook | Zastosowanie | Zwraca | |------|--------------|--------| |
useToggle | Przełącznik on/off | { value, toggle, setTrue, setFalse } |
| useCounter | Licznik z min/max | { count, increment, decrement, reset } |
| useFetch | Pobieranie danych | { data, loading, error, refetch } |
| useDebounce | Opóźnianie wartości | Opóźniona wartość |
| usePrevious | Poprzednia wartość | Poprzednia wartość |
| useArray | Operacje na tablicach | { array, push, remove, update, clear } |
| useLocalStorage | Persystentny stan | [value, setValue] |Custom Hooks to fundament dobrze zorganizowanej aplikacji React:
use - obowiązkowy, pozwala Reactowi weryfikować regułyJak modułowe systemy kosmicznego statku - każdy hook to samodzielny moduł, który można zainstalować, skonfigurować i połączyć z innymi modułami w dowolnej konfiguracji.