Kurs JavaScript i React · Moduł 12: React i TypeScript
Typowanie useState i useRef
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); // booleanTypeScript 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. Kiedy konieczne jest jawne podanie typu generycznego w useState?
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: