JavaScript and React course Β· Module 12: React and TypeScript

Typing useState and useRef

5 min read
In this lesson5

The planet list starts empty, the selected planet doesn't exist at first, and the search field must get focus after a click. Without types, each of these situations ends with the question "what is actually in here?". React hooks are the engines of your spaceship. TypeScript lets you define what data flows through these engines, ensuring safe navigation.

Typing useState

The useState hook automatically infers the type from the initial value, but sometimes you need explicit typing. You pass the explicit type in angle brackets right after the hook name, for example useState<string> - this is called a generic parameter. Let's start with the cases where you don't need to write anything:

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

TypeScript looks at 0, "Apollo" and true and fixes the state type for good. The call setCount("lots") is rejected, even though we never wrote the word number.

It's different when the initial value doesn't reveal the target type. The selected planet starts as null, and from null alone the compiler can't guess that a Planet object will appear later. So we pass the union type Planet | null:

1// Explicit typing - needed when the initial value is null or undefined
2interface Planet {
3  id: number;
4  name: string;
5  type: string;
6}
7
8// State can be Planet or null
9const [selectedPlanet, setSelectedPlanet] = useState<Planet | null>(null);
10
11// Array of objects - empty array doesn't tell TypeScript about element type
12const [planets, setPlanets] = useState<Planet[]>([]);
13
14// Union types
15type Status = "idle" | "loading" | "success" | "error";
16const [status, setStatus] = useState<Status>("idle");

What did NOT change: in the running app useState behaves exactly the same. The generic only tells the compiler which values may be passed to the setter. Thanks to Status, the typo setStatus("loadnig") won't get through.

When is Explicit Typing Necessary?

Below is a cheat sheet of four situations where giving a type is worth it. It uses a User interface - an ordinary user interface built just like Planet. Also new is the FormData interface with an optional field, which you know from the previous lesson.

1// 1. Initial value is null/undefined
2const [user, setUser] = useState<User | null>(null);
3
4// 2. Empty array
5const [items, setItems] = useState<string[]>([]);
6
7// 3. Union types with restricted values
8const [theme, setTheme] = useState<"dark" | "light">("dark");
9
10// 4. Complex object with optional fields
11interface FormData {
12  name: string;
13  email: string;
14  message?: string;
15}
16const [form, setForm] = useState<FormData>({ name: "", email: "" });

In case 3, without the generic TypeScript would treat theme as any string. In case 4, the generic lets you skip message at the start and add it later. Practical advice: when the initial value clearly shows the type, don't write the generic - less noise, more readable code.

Typing useRef

The useRef hook stores references to DOM elements or values that don't cause re-renders. For DOM elements you pass the browser element type, for example HTMLInputElement for a text field, and null as the initial value, because the element doesn't exist yet during the first render:

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

In React 19 the useRef argument is mandatory - useRef<HTMLInputElement>() alone is a type error. Here's a ref inside a component. The ?. operator (optional chaining) calls the method only when the value on its left is not null or undefined:

1function SearchBar() {
2  const inputRef = useRef<HTMLInputElement>(null);
3
4  const focusInput = () => {
5    // inputRef.current may be null, so we use ?.
6    inputRef.current?.focus();
7  };
8
9  return (
10    <div>
11      <input ref={inputRef} placeholder="Search for a planet..." />
12      <button onClick={focusInput}>Focus</button>
13    </div>
14  );
15}

React writes the element into inputRef.current after the component mounts, which is why current has the type HTMLInputElement | null. The ref attribute in JSX accepts this ref without any extra casts.

useRef as a Value "Container"

useRef can also hold any value that doesn't trigger a re-render - a timer ID, a previous value, a call counter. Changing current doesn't refresh the screen, unlike the useState setter.

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>Countdown: {seconds}s</p>
24      <button onClick={start}>Start</button>
25      <button onClick={stop}>Stop</button>
26    </div>
27  );
28}
29
30// Previous value
31const prevValueRef = useRef<string>("");

Why window.setInterval and not plain setInterval? In the browser the function returns a number, but when Node.js types are also in the project, plain setInterval may be typed as a Timeout object. The window. prefix removes the doubt and fits number | null. In React 19 all refs are mutable, so assigning to timerRef.current is fully legal.

Common HTML Elements in useRef

Finally, a cheat sheet of element types that most often end up in refs. The name always starts with HTML and ends with Element:

1// Most commonly used element types
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);

You don't need to memorize them - the editor suggests the name once you type HTML. You'll meet the same element types in the next lesson as the parameter of event types.

Code for this lesson: App.tsx
1import React, { useState, useRef } from 'react';
2
3// Planet interface
4interface Planet {
5  id: number;
6  name: string;
7  type: string;
8  distanceLy: number;
9}
10
11function App() {
12  // useState with automatic type inference
13  const [count, setCount] = useState(0);
14
15  // useState with an explicit type - initial value null
16  const [selectedPlanet, setSelectedPlanet] = useState<Planet | null>(null);
17
18  // useState with an array of objects
19  const [planets, setPlanets] = useState<Planet[]>([
20    { id: 1, name: "Mars", type: "rocky", distanceLy: 0.000024 },
21    { id: 2, name: "Jupiter", type: "gaseous", distanceLy: 0.000082 },
22    { id: 3, name: "Kepler-442b", type: "rocky", distanceLy: 1206 },
23  ]);
24
25  // useState with a union type
26  const [status, setStatus] = useState<"idle" | "scanning" | "complete">("idle");
27
28  // useRef for a DOM element
29  const searchRef = useRef<HTMLInputElement>(null);
30
31  // useRef as a container for a value (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>Cosmic Scanner</h1>
63
64      <div className="status-bar">
65        <span style={{ color: statusColors[status] }}>
66          Status: {status.toUpperCase()}
67        </span>
68        <span>Completed scans: {count}</span>
69      </div>
70
71      <div className="controls">
72        <input ref={searchRef} placeholder="Search for a planet..." className="search" />
73        <button onClick={focusSearch}>Focus</button>
74        <button onClick={startScan} disabled={status === "scanning"}>
75          {status === "scanning" ? "Scanning..." : "Scan"}
76        </button>
77        {status === "scanning" && (
78          <button onClick={cancelScan} className="cancel">Cancel</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>Type: {planet.type}</p>
91            <p>Distance: {planet.distanceLy} ly</p>
92          </div>
93        ))}
94      </div>
95
96      {selectedPlanet && (
97        <div className="detail">
98          <h2>Selected: {selectedPlanet.name}</h2>
99          <p>Type: {selectedPlanet.type}</p>
100          <p>Distance: {selectedPlanet.distanceLy} light-years</p>
101          <button onClick={() => setSelectedPlanet(null)}>Close</button>
102        </div>
103      )}
104    </div>
105  );
106}
107
108export default App;

Remember: useState powers what the crew sees on screen, while useRef works quietly in the engine room - both deserve a precise type.

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. When is it necessary to explicitly specify the generic type in useState?

  2. 2. How do you correctly type useRef to store a reference to an input element?

Hands-on tasks in the game

  • Code editor

    Typing useState with different types

  • Click in order

    Arrange the syntax for extending an interface in TypeScript:

  • Code editor

    Typing useRef for DOM elements and values

  • Vertical ordering

    Arrange the steps for creating typed state in React in the correct order:

  • Horizontal ordering

    Arrange the syntax of useState with an explicit generic type:

Useful articles