Kurs JavaScript i React · Moduł 12: React i TypeScript

Typowanie useState i useRef

5 min czytania
W tej lekcji5

Lista planet startuje pusta, wybrana planeta na początku nie istnieje, a pole wyszukiwania trzeba ustawić w fokusie po kliknięciu. Bez typów każda z tych sytuacji kończy się pytaniem "co tu właściwie jest?". Hooki React to silniki Twojego kosmicznego statku. TypeScript pozwala określić, jakie dane przepływają przez te silniki, zapewniając bezpieczną nawigację.

Typowanie useState

Hook useState automatycznie dedukuje typ na podstawie wartości początkowej, ale czasem potrzebujesz jawnego typowania. Jawny typ podajesz w nawiasach ostrych zaraz po nazwie hooka, na przykład useState<string> - to tak zwany parametr generyczny. Zacznijmy od przypadków, w których nie trzeba nic pisać:

1import { useState } from 'react';
2
3// Automatyczna dedukcja typu
4const [count, setCount] = useState(0);         // number
5const [name, setName] = useState("Apollo");    // string
6const [active, setActive] = useState(true);    // boolean

TypeScript patrzy na 0, "Apollo" i true i ustala typ stanu na zawsze. Wywołanie setCount("dużo") zostanie odrzucone, choć nigdzie nie napisaliśmy słowa number.

Inaczej jest, gdy wartość początkowa nie zdradza docelowego typu. Wybrana planeta na starcie to null, a z samego null kompilator nie wywnioskuje, że później pojawi się tam obiekt Planet. Wtedy podajemy union type Planet | null:

1// Jawne typowanie - potrzebne gdy początkowa wartość to null lub undefined
2interface Planet {
3  id: number;
4  name: string;
5  type: string;
6}
7
8// Stan może być Planet lub null
9const [selectedPlanet, setSelectedPlanet] = useState<Planet | null>(null);
10
11// Tablica obiektów - pusta tablica nie mówi TypeScript o typie elementów
12const [planets, setPlanets] = useState<Planet[]>([]);
13
14// Union types
15type Status = "idle" | "loading" | "success" | "error";
16const [status, setStatus] = useState<Status>("idle");

Co się NIE zmieniło: w działającej aplikacji useState zachowuje się dokładnie tak samo. Generyk tylko mówi kompilatorowi, jakie wartości wolno przekazać do settera. Dzięki Status literówka setStatus("loadnig") nie przejdzie.

Kiedy jawne typowanie jest konieczne?

Poniżej ściąga czterech sytuacji, w których warto podać typ. Występuje w niej interfejs User - to zwykły interfejs użytkownika, zbudowany tak samo jak Planet. Nowy jest też interfejs FormData z polem opcjonalnym, które znasz z poprzedniej lekcji.

1// 1. Początkowa wartość null/undefined
2const [user, setUser] = useState<User | null>(null);
3
4// 2. Pusta tablica
5const [items, setItems] = useState<string[]>([]);
6
7// 3. Union types z ograniczonymi wartościami
8const [theme, setTheme] = useState<"dark" | "light">("dark");
9
10// 4. Złożony obiekt z opcjonalnymi polami
11interface FormData {
12  name: string;
13  email: string;
14  message?: string;
15}
16const [form, setForm] = useState<FormData>({ name: "", email: "" });

W punkcie 3 bez generyka TypeScript uznałby theme za dowolny string. W punkcie 4 generyk pozwala pominąć message na starcie i dodać je później. Rada praktyczna: gdy wartość początkowa jasno pokazuje typ, nie pisz generyka - im mniej szumu, tym czytelniejszy kod.

Typowanie useRef

Hook useRef służy do przechowywania referencji do elementów DOM lub wartości, które nie powodują ponownego renderowania. Dla elementów DOM podajesz typ elementu przeglądarki, na przykład HTMLInputElement dla pola tekstowego, a jako wartość początkową null, bo element jeszcze nie istnieje w chwili pierwszego renderu:

1import { useRef } from 'react';
2
3// Referencja do elementu DOM
4const inputRef = useRef<HTMLInputElement>(null);
5const divRef = useRef<HTMLDivElement>(null);
6const canvasRef = useRef<HTMLCanvasElement>(null);

W React 19 argument useRef jest obowiązkowy - samo useRef<HTMLInputElement>() zgłosi błąd typów. Tak wygląda referencja w komponencie. Operator ?. (optional chaining) wywołuje metodę tylko wtedy, gdy wartość po lewej nie jest null ani undefined:

1function SearchBar() {
2  const inputRef = useRef<HTMLInputElement>(null);
3
4  const focusInput = () => {
5    // inputRef.current może być null, więc używamy ?.
6    inputRef.current?.focus();
7  };
8
9  return (
10    <div>
11      <input ref={inputRef} placeholder="Szukaj planety..." />
12      <button onClick={focusInput}>Fokus</button>
13    </div>
14  );
15}

React wpisuje element do inputRef.current po zamontowaniu komponentu, dlatego typ current to HTMLInputElement | null. Atrybut ref w JSX akceptuje tę referencję bez dodatkowych rzutowań.

useRef jako "kontener" na wartość

useRef może również przechowywać dowolną wartość, która nie powoduje ponownego renderowania - identyfikator timera, poprzednią wartość, licznik wywołań. Zmiana current nie odświeża ekranu, w przeciwieństwie do settera z useState.

1// Timer ID
2const timerRef = useRef<number | null>(null);
3
4function Countdown() {
5  const timerRef = useRef<number | null>(null);
6  const [seconds, setSeconds] = useState(10);
7
8  const start = () => {
9    timerRef.current = window.setInterval(() => {
10      setSeconds(prev => prev - 1);
11    }, 1000);
12  };
13
14  const stop = () => {
15    if (timerRef.current !== null) {
16      clearInterval(timerRef.current);
17      timerRef.current = null;
18    }
19  };
20
21  return (
22    <div>
23      <p>Odliczanie: {seconds}s</p>
24      <button onClick={start}>Start</button>
25      <button onClick={stop}>Stop</button>
26    </div>
27  );
28}
29
30// Poprzednia wartość
31const prevValueRef = useRef<string>("");

Dlaczego window.setInterval, a nie samo setInterval? W przeglądarce funkcja zwraca liczbę, ale gdy w projekcie są też typy Node.js, samo setInterval bywa typowane jako obiekt Timeout. Przedrostek window. rozwiewa tę wątpliwość i pasuje do number | null. W React 19 wszystkie referencje są modyfikowalne, więc przypisanie do timerRef.current jest w pełni legalne.

Typowe elementy HTML w useRef

Na koniec ściąga typów elementów, które najczęściej trafiają do referencji. Nazwa zawsze zaczyna się od HTML i kończy na Element:

1// Najczęściej używane typy elementów
2const inputRef = useRef<HTMLInputElement>(null);
3const textareaRef = useRef<HTMLTextAreaElement>(null);
4const selectRef = useRef<HTMLSelectElement>(null);
5const buttonRef = useRef<HTMLButtonElement>(null);
6const formRef = useRef<HTMLFormElement>(null);
7const divRef = useRef<HTMLDivElement>(null);
8const imgRef = useRef<HTMLImageElement>(null);
9const videoRef = useRef<HTMLVideoElement>(null);

Nie musisz ich zapamiętywać - edytor podpowie nazwę po wpisaniu HTML. Te same typy elementów spotkasz w następnej lekcji jako parametr typów zdarzeń.

Kod do tej lekcji: App.tsx
1import React, { useState, useRef } from 'react';
2
3// Interfejs planety
4interface Planet {
5  id: number;
6  name: string;
7  type: string;
8  distanceLy: number;
9}
10
11function App() {
12  // useState z automatyczna dedukcja
13  const [count, setCount] = useState(0);
14
15  // useState z jawnym typem - poczatkowa wartosc null
16  const [selectedPlanet, setSelectedPlanet] = useState<Planet | null>(null);
17
18  // useState z tablica obiektow
19  const [planets, setPlanets] = useState<Planet[]>([
20    { id: 1, name: "Mars", type: "skalista", distanceLy: 0.000024 },
21    { id: 2, name: "Jupiter", type: "gazowa", distanceLy: 0.000082 },
22    { id: 3, name: "Kepler-442b", type: "skalista", distanceLy: 1206 },
23  ]);
24
25  // useState z union type
26  const [status, setStatus] = useState<"idle" | "scanning" | "complete">("idle");
27
28  // useRef do elementu DOM
29  const searchRef = useRef<HTMLInputElement>(null);
30
31  // useRef jako kontener na wartosc (timer)
32  const scanTimerRef = useRef<number | null>(null);
33
34  const focusSearch = () => {
35    searchRef.current?.focus();
36  };
37
38  const startScan = () => {
39    setStatus("scanning");
40    scanTimerRef.current = window.setTimeout(() => {
41      setStatus("complete");
42      setCount(prev => prev + 1);
43    }, 2000);
44  };
45
46  const cancelScan = () => {
47    if (scanTimerRef.current !== null) {
48      clearTimeout(scanTimerRef.current);
49      scanTimerRef.current = null;
50      setStatus("idle");
51    }
52  };
53
54  const statusColors = {
55    idle: "#9ca3af",
56    scanning: "#f59e0b",
57    complete: "#22c55e"
58  };
59
60  return (
61    <div className="app">
62      <h1>Skaner Kosmiczny</h1>
63
64      <div className="status-bar">
65        <span style={{ color: statusColors[status] }}>
66          Status: {status.toUpperCase()}
67        </span>
68        <span>Ukonczone skany: {count}</span>
69      </div>
70
71      <div className="controls">
72        <input ref={searchRef} placeholder="Szukaj planety..." className="search" />
73        <button onClick={focusSearch}>Fokus</button>
74        <button onClick={startScan} disabled={status === "scanning"}>
75          {status === "scanning" ? "Skanowanie..." : "Skanuj"}
76        </button>
77        {status === "scanning" && (
78          <button onClick={cancelScan} className="cancel">Anuluj</button>
79        )}
80      </div>
81
82      <div className="planets">
83        {planets.map(planet => (
84          <div
85            key={planet.id}
86            className={"planet-card " + (selectedPlanet?.id === planet.id ? "selected" : "")}
87            onClick={() => setSelectedPlanet(planet)}
88          >
89            <h3>{planet.name}</h3>
90            <p>Typ: {planet.type}</p>
91            <p>Odleglosc: {planet.distanceLy} ly</p>
92          </div>
93        ))}
94      </div>
95
96      {selectedPlanet && (
97        <div className="detail">
98          <h2>Wybrana: {selectedPlanet.name}</h2>
99          <p>Typ: {selectedPlanet.type}</p>
100          <p>Odleglosc: {selectedPlanet.distanceLy} lat swietlnych</p>
101          <button onClick={() => setSelectedPlanet(null)}>Zamknij</button>
102        </div>
103      )}
104    </div>
105  );
106}
107
108export default App;

Pamiętaj: useState napędza to, co widzi załoga na ekranie, a useRef pracuje cicho w maszynowni - oba zasługują na precyzyjny typ.

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. Kiedy konieczne jest jawne podanie typu generycznego w useState?

  2. 2. Jak poprawnie typować useRef do przechowywania referencji do elementu input?

Zadania praktyczne w grze

  • Edytor kodu

    Typowanie useState z różnymi typami

  • Klikanie w kolejności

    Ułóż składnię rozszerzania interfejsu w TypeScript:

  • Edytor kodu

    Typowanie useRef dla elementów DOM i wartości

  • Układanie w pionie

    Ułóż kolejność kroków tworzenia typowanego stanu w React:

  • Układanie w poziomie

    Ułóż składnię useState z jawnym typem generycznym:

Przydatne artykuły