Typing React: Props, Hooks, Context & Generics

0/5 in this phase0/21 across the roadmap

📖 Concept

React and TypeScript are a natural fit — TypeScript catches prop misuse, hook errors, and context issues at compile time. Modern React (function components + hooks) is where TypeScript shines.

Component typing patterns:

  • React.FC<Props> — Functional component type (debated — many teams avoid it)
  • Direct annotation: function MyComponent(props: Props): JSX.Element
  • React.PropsWithChildren<Props> — Add children to your props

Hook typing:

  • useState<T>() — Usually inferred, but specify for complex/union state
  • useRef<T>(null) — The generic determines what the ref points to
  • useReducer — Discriminated unions for actions → exhaustive type safety

Event typing:

  • React.ChangeEvent<HTMLInputElement> — Input change events
  • React.FormEvent<HTMLFormElement> — Form submission
  • React.MouseEvent<HTMLButtonElement> — Click events

Generic components let you build reusable components where the data type flows through — like a <List<T>> that knows the type of each item.

🏠 Real-world analogy: TypeScript in React is like having a building inspector review your blueprints (props) before construction (rendering). Wrong wiring (prop types)? Caught before the walls go up, not after a fire.

💻 Code Example

codeTap to expand ⛶
1// Component with typed props
2interface ButtonProps {
3 label: string;
4 variant?: "primary" | "secondary" | "danger";
5 disabled?: boolean;
6 onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
7}
8
9function Button({ label, variant = "primary", disabled, onClick }: ButtonProps) {
10 return (
11 <button
12 className={`btn btn-${variant}`}
13 disabled={disabled}
14 onClick={onClick}
15 >
16 {label}
17 </button>
18 );
19}
20
21// Children typing
22interface CardProps {
23 title: string;
24 children: React.ReactNode; // Accepts anything renderable
25}
26
27function Card({ title, children }: CardProps) {
28 return (
29 <div className="card">
30 <h2>{title}</h2>
31 {children}
32 </div>
33 );
34}
35
36// ⭐ useState with unions (discriminated union state)
37type FormState =
38 | { status: "idle" }
39 | { status: "submitting" }
40 | { status: "success"; data: { id: string } }
41 | { status: "error"; error: string };
42
43function useFormSubmit() {
44 const [state, setState] = React.useState<FormState>({ status: "idle" });
45
46 async function submit(data: FormData) {
47 setState({ status: "submitting" });
48 try {
49 const result = await fetch("/api/submit", { method: "POST", body: data });
50 const json = await result.json();
51 setState({ status: "success", data: json });
52 } catch (e) {
53 setState({ status: "error", error: (e as Error).message });
54 }
55 }
56 return { state, submit };
57}
58
59// useRef typing
60function TextInput() {
61 const inputRef = React.useRef<HTMLInputElement>(null);
62
63 function focusInput() {
64 inputRef.current?.focus(); // Optional chaining — ref might be null
65 }
66
67 return <input ref={inputRef} />;
68}
69
70// useReducer with discriminated union actions
71type CounterAction =
72 | { type: "increment" }
73 | { type: "decrement" }
74 | { type: "reset"; payload: number };
75
76function counterReducer(state: number, action: CounterAction): number {
77 switch (action.type) {
78 case "increment": return state + 1;
79 case "decrement": return state - 1;
80 case "reset": return action.payload;
81 }
82}
83
84// ⭐ Generic component
85interface ListProps<T> {
86 items: T[];
87 renderItem: (item: T, index: number) => React.ReactNode;
88 keyExtractor: (item: T) => string;
89}
90
91function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
92 return (
93 <ul>
94 {items.map((item, i) => (
95 <li key={keyExtractor(item)}>{renderItem(item, i)}</li>
96 ))}
97 </ul>
98 );
99}
100
101// Usage — T is inferred as User!
102// <List
103// items={users}
104// renderItem={(user) => <span>{user.name}</span>}
105// keyExtractor={(user) => user.id.toString()}
106// />
107
108// Context with TypeScript
109interface AuthContext {
110 user: { id: string; name: string } | null;
111 login: (email: string, password: string) => Promise<void>;
112 logout: () => void;
113}
114
115const AuthContext = React.createContext<AuthContext | null>(null);
116
117function useAuth(): AuthContext {
118 const ctx = React.useContext(AuthContext);
119 if (!ctx) throw new Error("useAuth must be used within AuthProvider");
120 return ctx;
121}

🏋️ Practice Exercise

Mini Exercise:

  1. Create a typed form component with onChange handlers for different input types
  2. Build a generic <Select<T>> component where options and selected value are typed
  3. Use useReducer with a discriminated union of 5+ action types
  4. Create a typed Context + custom hook pattern with proper null-safety
  5. Type a useDebounce<T> hook that preserves the generic type of the debounced value

⚠️ Common Mistakes

  • Using React.FC and relying on its implicit children — React 18 removed implicit children from FC; always declare children explicitly in props

  • Typing state as the initial value's type only — useState('') gives string, but if state can be null, use useState<string | null>(null)

  • Forgetting that useRef<T>(null) starts as null — always use optional chaining ref.current?.method() or check for null

  • Creating context without a null check — createContext(undefined as any) is unsafe; use a custom hook that throws if context is null

  • Over-typing event handlers — (e: React.ChangeEvent<HTMLInputElement>) => void on every handler; often the inline type is inferred

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Typing React: Props, Hooks, Context & Generics