Careeroza
Typing Hooks
useState and useRef
"use client";
import { useEffect, useRef, useState } from "react";
type User = { id: string; name: string };
export function Profile() {
const [count, setCount] = useState(0); // inferred: number
const [user, setUser] = useState<User | null>(null); // needs a type: starts as null
const [tags, setTags] = useState<string[]>([]); // empty array needs a type
const inputRef = useRef<HTMLInputElement>(null); // DOM ref: RefObject<HTMLInputElement | null>
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined); // mutable value
useEffect(() => {
inputRef.current?.focus();
timerRef.current = setTimeout(() => setCount((c) => c + 1), 1000);
return () => clearTimeout(timerRef.current);
}, []);
return <input ref={inputRef} defaultValue={user?.name ?? ""} data-tags={tags.length} />;
}useReducer With a Discriminated Union
Typing actions as a discriminated union gives the reducer exhaustive, narrowed cases. The reducer itself is plain TypeScript, so it can be run and tested without React:
Example
type CartItem = { sku: string; name: string; price: number; qty: number };
type CartState = { items: CartItem[]; coupon: string | null };
type CartAction =
| { type: "add"; item: Omit<CartItem, "qty"> }
| { type: "setQty"; sku: string; qty: number }
| { type: "remove"; sku: string }
| { type: "applyCoupon"; code: string };
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case "add": {
const found = state.items.find((i) => i.sku === action.item.sku);
return found
? { ...state, items: state.items.map((i) => (i.sku === action.item.sku ? { ...i, qty: i.qty + 1 } : i)) }
: { ...state, items: [...state.items, { ...action.item, qty: 1 }] };
}
case "setQty":
return { ...state, items: state.items.map((i) => (i.sku === action.sku ? { ...i, qty: action.qty } : i)) };
case "remove":
return { ...state, items: state.items.filter((i) => i.sku !== action.sku) };
case "applyCoupon":
return { ...state, coupon: action.code.toUpperCase() };
}
}
const actions: CartAction[] = [
{ type: "add", item: { sku: "A", name: "Kettle", price: 1299 } },
{ type: "add", item: { sku: "B", name: "Mug", price: 199 } },
{ type: "add", item: { sku: "B", name: "Mug", price: 199 } },
{ type: "setQty", sku: "A", qty: 2 },
{ type: "remove", sku: "B" },
{ type: "applyCoupon", code: "fest20" },
];
const final = actions.reduce(cartReducer, { items: [], coupon: null });
console.log(final.items.map((i) => `${i.name} x${i.qty}`), final.coupon);Output
[ 'Kettle x2' ] FEST20
// in the component
const [cart, dispatch] = useReducer(cartReducer, { items: [], coupon: null });
dispatch({ type: "setQty", sku: "A", qty: 2 }); // OK
dispatch({ type: "setQty", sku: "A" }); // error: qty missing