Strona główna / Artykuły / Pisanie hooków w React: useState, useEffect, useReducer oraz custom hooks

Pisanie hooków w React: useState, useEffect, useReducer oraz custom hooks

Dowiedz się, jak poprawnie pisać useState, useEffect, useReducer oraz własne hooki w TypeScriptie, a także kiedy wybór TypeScripta zamiast zwykłego JavaScripta faktycznie się opłaca.

1342 słów

Błędy typowe wykrywane w czasie kompilacji chronią przed sesjami debugowania, które inaczej mogłyby trwać aż do późnej nocy, i właśnie dlatego zespoły łączące React z TypeScript traktują hooki typowane jako element podstawowy, a nie luksus. Opanowanie sposobu definiowania typów dla stanu, efektów, reduktorów oraz własnych hooków — oraz wiedza o tym, kiedy użycie TypeScript jest nawet właściwym rozwiązaniem — to dwie strony tej samej praktycznej umiejętności niezbędnej do tworzenia niezawodnych aplikacji React.

Dlaczego bezpieczeństwo typów powinno być częścią twoich hooki

Hooki React już dają programistom czystszy i bardziej elastyczny sposób na strukturyzowanie komponentów. TypeScript dodaje warstwę bezpieczeństwa na wierzchu tej struktury, dzięki czemu błędy pojawiają się podczas pisania kodu, a nie dopiero wtedy, gdy użytkownik natrafi na uszkodzony interfejs w środowisku produkcyjnym.

Hooks zostały zaprojektowane od samego początku tak, aby logika wielokrotnego użycia była przewidywalna — na ten temat mówił Dan Abramov, omawiając filozofię projektową React. Zasługą TypeScript jest to, że sprawia ona, iż ta przewidywalność staje się jawna i poddawalna weryfikacji, zamiast być czymś, co trzeba mieć w pamięci.

Prawidłowe definiowanie typu useState

W większości codziennych przypadków TypeScript sama określa typ stanu bez żadnej pomocy:

// inferred as boolean, no annotation needed
const [isLoading, setIsLoading] = useState(false);

Sytuacja zmienia się, gdy wartość początkowa to null lub undefined — w takim przypadku musisz samodzielnie określić typ, zamiast polegać na automatycznej inferycji:

interface UserProfile {
  id: string;
  name: string;
  avatarUrl: string;
}

const [user, setUser] = useState<UserProfile | null>(null);

Przydatnym nawykiem jest opieranie się pokusie użycia typu ogólnego tylko po to, by usunąć komunikat o błędzie. Robienie tego potajemnie eliminuje całą korzyść, jaką chciałeś uzyskać dzięki TypeScript.

Typowanie funkcji useEffect: Mniej skomplikowane, niż się wydaje

Generyka tak naprawdę nie odnosi się do useEffect, ale nadal trzeba uważać na tablice zależności i logikę czyszczenia:

useEffect(() => {
  const controller = new AbortController();

  const fetchUser = async () => {
    const res = await fetch(`/api/users/${userId}`, {
      signal: controller.signal,
    });
    const data: UserProfile = await res.json();
    setUser(data);
  };

  fetchUser();

  return () => controller.abort(); // cleanup on unmount
}, [userId]);

Typowanie useReducer dla bardziej złożonego stanu

To właśnie w tym hooku wartość TypeScript staje się oczywista:

type CartAction =
  | { type: 'ADD_ITEM'; payload: CartItem }
  | { type: 'REMOVE_ITEM'; payload: string }
  | { type: 'CLEAR_CART' };

function cartReducer(state: CartItem[], action: CartAction): CartItem[] {
  switch (action.type) {
    case 'ADD_ITEM':
      return [...state, action.payload];
    case 'REMOVE_ITEM':
      return state.filter((item) => item.id !== action.payload);
    case 'CLEAR_CART':
      return [];
    default:
      return state;
  }
}

Gdy typy działań są modelowane w ten sposób, wysłanie nieprawidłowego danych powoduje błąd kompilacji, który można natychmiast wykryć, zamiast niespodzianki w czasie wykonywania programu, o której dowiadujemy się później.

Pisanie wielokrotnie używalnych hooków niestandardowych z użyciem generyk

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = window.localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

Ponieważ ten hook jest generyczny, można go wielokrotnie używać w całym kodzie — z ciągami znaków, obiektami lub tablicami — zachowując pełną dokładność typów dla wszystkich danych, które przez niego przekazujemy.

Wersja krótka

  • Pozwól TypeScriptowi samodzielnie wywnioskować prosty stan, ale bądź precyzyjny, gdy wartość może być null lub undefined.
  • Zamodeluj swoje akcje useReducer jako zbiór rozróżniony.
  • Używaj generyków w niestandardowych hookach, aby mogły być ponownie wykorzystywane z różnymi typami danych.
  • Traktuj any jako skrót, który potajemnie kosztuje cię więcej, niż później oszczędza.

Wybór między zwykłym JavaScript a TypeScript

Gdy już zrozumiesz, jak wiele bezpieczeństwa dają hooki typowane, naturalnie pojawia się szersze pytanie: czy każdy projekt powinien faktycznie używać TypeScript, czy to czasami jest nadmiernym projektowaniem? Przez długi czas było to przedstawiane jako wybór binarny, a wybranie jednej ze stron oznaczało konieczność pogodzenia się z rzeczywistymi kompromisami.

Kiedyś wybór TypeScript oznaczał konieczność radzenia sobie z procesami budowania, narzędziami takimi jak ts-node oraz mnóstwem plików konfiguracyjnych, aby tylko uruchomić skrypt. Teraz, gdy środowiska wykonawcze takie jak Node.js obsługują usuwanie typów natywnych, te trudności w dużej mierze zniknęły, a granica pomiędzy pisaniem zwykłego JavaScript a TypeScript jest znacznie cieńsza niż kiedyś.

Rozważenie wpływu każdej z opcji na codzienną pracę ułatwia podjęcie decyzji o tym, która faktycznie pasuje do danego projektu.

Dlaczego zwykły JavaScript nadal ma swoje miejsce

JavaScript to język, który przeglądarka i Node.js rozumieją w sposób natywny. Piszesz plik, uruchamiasz go i ten jest natychmiast wykonywany – bez żadnego kompilatora, który stanowiłby przeszkodę, i bez konieczności deklarowania czegokolwiek dodatkowego.

  • Błyskawiczne tworzenie prototypów: gdy testujesz pomysł, szybko piszesz skrypt lub tworzysz małe MVP, JavaScript umożliwia ci działanie tak szybko, jak tylko potrafisz myśleć.
  • Brak konieczności konfiguracji: nie jest potrzebny plik tsconfig.json ani krok weryfikacji typów, aby tylko upewnić się, że funkcja działa tak, jak oczekujesz.
  • Mniejszy obciążenie umysłowe: twoja uwaga skupia się na rzeczywistej logice programu, a nie na deklaracjach typów czy komunikatach od kompilatora.

Wady pojawiają się, gdy baza kodu napisanego w JavaScript przekracza kilka tysięcy linii. W takim przypadku refaktoryzacja staje się ryzykowna – przemianowanie właściwości w dwudziestu plikach oznacza konieczność użycia globalnego narzędzia do wyszukiwania i zastępowania oraz nadzieję, że nic nie złamie się w tle podczas uruchamiania aplikacji.

Dlaczego TypeScript się opłaca w miarę rozwoju projektów

TypeScript nakłada statyczny system typów na JavaScript, działając jak automatyczny recenzent, który wskazuje na problemy podczas pisania — na przykład ostrzega, gdy tylko próbujesz przekazać ciąg znaków do funkcji oczekującej liczby.

  • Dokumentacja, która pozostaje dokładna: typy pełnią rolę żywej dokumentacji. Interfejs informuje o dokładnej strukturze, jaką musi mieć obiekt, bez konieczności przeglądania kilku plików z implementacją.
  • Bezpieczniejsze refaktoryzacje: jeśli zmienisz model danych lub schemat bazy danych, kompilator wskazuje na wszystkie miejsca w kodzie, które wymagają aktualizacji.
  • Lepsze wsparcie edytora: narzędzia takie jak VS Code lub Cursor korzystają z serwera językowego TypeScript, aby zapewniać szybkie uzupełnianie tekstu i wyróżnianie błędów w trakcie pracy.

Historcznie kompromis ten polegał na rzeczywistym „podatku od narzędzi” – konfiguracja kompilatorów, map źródłowych oraz skryptów otaczających dodawała dodatkowe kroki do tego, co kiedyś było prostym procesem pracy.

Dlaczego ten kompromis już nie obowiązuje w taki sam sposób

Dawny zarzut, że „konfiguracja TypeScript jest zbyt uciążliwa”, nie ma już tak mocnego uzasadnienia. Obecne wersje Node.js mogą uruchamiać pliki TypeScript bezpośrednio, bez osobnego kroku budowania, dzięki usuwaniu informacji typów.

W tle silnik wykonawczy po prostu usuwa twoje adnotacje typów i deklaracje interfejsów, pozostawiając zwykły JavaScript do natychmiastowego uruchomienia. Oznacza to, że zachowujesz bezpieczeństwo typów statycznych charakterystyczne dla fazy rozwoju, bez dodatkowego obciążenia związanego z konfiguracją, które kiedyś towarzyszyło temu procesowi.

Dopasowanie narzędzia do zadania

Dla skryptu jednorazowego, małego zadań automatyzacyjnych lub projektu przeznaczonego wyłącznie do nauki, jak działa sieć, zwykły JavaScript pozostaje lepszym wyborem. Pominięcie dodatkowej struktury sprawia, że proces jest prosty i przyjemny.

Dla niemal wszystkiego innego — baz kodu zespołowych, aplikacji z kilkoma wzajemnie oddziałującymi modelami danych lub jakiegokolwiek projektu, który planujesz utrzymywać dłużej niż miesiąc — niewielka ilość konfiguracji wymagana przez TypeScript jest warta zachodu. Ponieważ nowoczesne środowiska wykonawcze zarówno w jednym, jak i drugim przypadku zapewniają płynną eksploatację, nie musisz faktycznie wybierać między elastycznością a bezpieczeństwem: zarówno prostota JavaScript, jak i zasady bezpieczeństwa TypeScript są dostępne, a wybór jednego z nich polega głównie na dopasowaniu go do tego, jak długo i w jaki sposób współpracowo będzie realizowany projekt.

Literatura pokrewna