Last 30 Days
No notifications
React + TypeScript is the dominant front-end stack — and the one this very project uses. You'll type props, state, refs, events, children, and generic components. Get this right and your IDE basically writes the JSX for you.
Use a type or interface for props. Both work — convention is Props suffix:
type ButtonProps = {
label: string;
onClick: () => void;
disabled?: boolean;
};function Button({ label, onClick, disabled }: ButtonProps) {
return <button onClick={onClick} disabled={disabled}>{label}</button>;
}
You almost never need React.FC anymore — it adds an implicit children prop and obscures generics.
children and ReactNodetype CardProps = {
title: string;
children: React.ReactNode; // anything renderable
};function Card({ title, children }: CardProps) {
return <section><h2>{title}</h2>{children}</section>;
}
ReactNode covers strings, numbers, JSX, arrays, fragments, null. Use ReactElement only when you need a single JSX element.
useStateconst [count, setCount] = useState(0); // inferred number
const [user, setUser] = useState<User | null>(null); // explicit when initial doesn't carry the full shapeuseReducerType the state and the action union — narrowing then works inside the reducer:
type State = { count: number };
type Action =
| { type: "inc" }
| { type: "dec" }
| { type: "set"; value: number };function reducer(state: State, action: Action): State {
switch (action.type) {
case "inc": return { count: state.count + 1 };
case "dec": return { count: state.count - 1 };
case "set": return { count: action.value };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
useRefTwo flavours:
const inputRef = useRef<HTMLInputElement>(null); // DOM ref
const idRef = useRef(0); // mutable valueDOM refs are null until React attaches them — TS makes you check.
useEffect / useCallback / useMemoMostly inferred, but type their generics when the inferred type isn't tight enough:
const onSubmit = useCallback<(e: FormEvent) => void>((e) => { /* … */ }, []);
const total = useMemo<number>(() => items.reduce((s, x) => s + x.price, 0), [items]);Always grab the event type from React:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log(e.currentTarget.dataset.id);
}
Cheat sheet:
| Event | Type |
| Change | React.ChangeEvent |
| Submit | React.FormEvent |
| Click | React.MouseEvent |
| Key | React.KeyboardEvent |
| Focus | React.FocusEvent |
e.target is the actual element that fired the event; e.currentTarget is the element the handler is attached to. currentTarget is usually what you want.
Yes — components can be generic:
type ListProps<T> = {
items: T[];
render: (item: T) => React.ReactNode;
};function List<T>({ items, render }: ListProps<T>) {
return <ul>{items.map((it, i) => <li key={i}>{render(it)}</li>)}</ul>;
}
// Usage — T is inferred from items
<List
items={[{ id: 1, name: "Ada" }, { id: 2, name: "Linus" }]}
render={(u) => u.name} // u is { id: number; name: string }
/>
as const for tuple stateIf a custom hook returns a tuple, mark it as const so consumers get a tuple, not (T | F)[]:
function useToggle(init = false) {
const [on, setOn] = useState(init);
return [on, () => setOn((v) => !v)] as const;
// type: readonly [boolean, () => void]
}type InputProps = React.InputHTMLAttributes<HTMLInputElement>;const Input = React.forwardRef<HTMLInputElement, InputProps>(
function Input(props, ref) {
return <input ref={ref} {...props} />;
}
);
type for component props — supports unions, intersections, mapped types.React.FC unless you have a reason; it hurts generics & adds implicit children.useState.React.ComponentProps<"button"> to inherit native props instead of retyping them.tsc --noEmit in CI catches real bugs your tests won't.