React 与 TypeScript
用类型系统为组件加上安全护栏
🎯 学习目标
学完本章,你将能够:
- 能够为 React 组件的 Props、State 和事件处理添加类型注解
- 能够正确使用 useState、useRef、useReducer 等内置 Hook 的类型
- 能够使用泛型组件与 forwardRef 实现类型安全的组件复用
- 能够构建类型安全的 Context 与自定义 Hook
- 能够使用 React Router 类型与 react-hook-form 实现类型安全路由与表单
你有没有想过
为什么组件传错一个 prop,运行时才报错?能不能在写代码的时候就发现?
React 的 Props、State、事件处理——到处都是"数据流动"。如果类型不匹配,轻则页面显示异常,重则直接白屏。TypeScript 就是你的编译时安全网,写代码时就能发现类型错误。
本章从项目创建到 Hooks 类型、事件处理、Context 泛型,逐一讲透 React + TS 的核心用法。
Part 1: 项目与 Props
创建项目,定义组件的"输入接口"。
万事开头难。先把项目跑起来,再搞清楚怎么用 TypeScript 定义组件的 Props——这是 React + TS 的第一步。
学完本节,你会知道如何用 interface 约束组件参数,让编辑器在写代码时就帮你检查传参是否正确。
创建项目
一行命令拿到完整的 React + TS 项目
$ npm create vite@latest my-react-app -- --template react-ts
$ cd my-react-app && npm install && npm run dev
核心依赖:
| 依赖 | 作用 |
|---|---|
react | React 核心库 |
react-dom | React DOM 渲染器 |
@types/react | React 类型定义 |
@types/react-dom | ReactDOM 类型定义 |
项目结构:
src/
├── main.tsx # 入口:ReactDOM.createRoot
├── App.tsx # 根组件
├── components/ # 组件目录
├── hooks/ # 自定义 Hook
├── types/ # 类型定义
└── utils/ # 工具函数
组件 Props 类型
用 interface 定义组件的"输入规格"
Props 就是组件的参数。用 TypeScript 定义 Props 类型,编辑器就能帮你检查传参是否正确。
// 推荐:用 interface 定义 Props
interface UserCardProps {
name: string;
age: number;
email?: string; // 可选属性
onClick?: (id: string) => void; // 回调函数
}
function UserCard({ name, age, email, onClick }: UserCardProps) {
return (
<div onClick={() => onClick?.(name)}>
<h2>{name}</h2>
<p>{age}岁</p>
{email && <p>{email}</p>}
</div>
);
}
React.FC:虽然可以用 const Comp: React.FC<Props> 的写法,但现在社区更推荐直接用普通函数 + interface 的方式。更简洁,泛型支持也更好。
children 与常用类型
React 提供了一组专用类型
// children 的类型
interface LayoutProps {
children: React.ReactNode; // 最通用的 children 类型
title: string;
}
function Layout({ children, title }: LayoutProps) {
return (
<div>
<h1>{title}</h1>
{children}
</div>
);
}
| 类型 | 用途 |
|---|---|
React.ReactNode | 所有可渲染内容(string|number|JSX|null|undefined) |
React.ReactElement | JSX 元素 |
React.ComponentType | 组件类型(类组件或函数组件) |
React.CSSProperties | CSS 属性对象 |
React.Ref<T> | ref 类型 |
Part 2: Hooks 类型
useState、useReducer、useRef——每个 Hook 都有对应的类型写法。
Hooks 是 React 的灵魂。TypeScript 能为每个 Hook 提供精确的类型推断,让 dispatch、ref.current 等都不再是 any。
本节还会讲解 useEffect、useCallback、useMemo 的类型要点——它们各有各的坑。
useState 类型
简单类型自动推断,复杂类型手动指定
// 自动推断
const [count, setCount] = useState(0); // number
const [name, setName] = useState("张三"); // string
// 手动指定——联合类型或复杂类型
const [user, setUser] = useState<User | null>(null); // 需要手动指定
const [list, setList] = useState<string[]>([]); // 空数组需要指定
规则很简单:初始值能推断出类型就不用写,推断不出或推断错了就手动指定泛型参数。
useReducer 类型
用联合类型确保 dispatch 的类型安全
// 定义 Action 的联合类型
type Action =
| { type: "increment"; payload: number }
| { type: "decrement"; payload: number }
| { type: "reset" };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + action.payload;
case "decrement": return state - action.payload;
case "reset": return 0;
}
}
const [count, dispatch] = useReducer(reducer, 0);
dispatch({ type: "increment", payload: 5 }); // OK
// dispatch({ type: "increment" }); // 报错!缺少 payload
// dispatch({ type: "reset", payload: 0 }); // 报错!reset 不需要 payload
联合类型 + switch 穷举 = 完整的类型安全。编辑器会在你写 dispatch 的时候提示所有合法的 Action。
useRef 类型
三种典型用法
// 用法一:引用 DOM 元素
const inputRef = useRef<HTMLInputElement>(null);
const focus = () => inputRef.current?.focus();
return <input ref={inputRef} />;
// 用法二:保存可变值(定时器 ID 等)
const timerRef = useRef<ReturnType<typeof setInterval>>();
// 用法三:保存实例引用
const instanceRef = useRef<MyClass | null>(null);
useRef 和 useState 的关键区别:修改 ref.current 不会触发重新渲染。适合保存不影响 UI 的"幕后数据"。
useEffect 类型
副作用函数的返回值只能是 void 或清理函数
useEffect 的回调函数不能是 async——因为 async 函数返回 Promise,而 useEffect 期望返回 void | (() => void)。
// 返回类型:void | (() => void)
useEffect(() => {
const controller = new AbortController();
// 正确:在内部用 async
async function loadData() {
try {
const res = await fetch("/api/users", {
signal: controller.signal,
});
const data: User[] = await res.json();
setUsers(data);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
return; // 组件卸载,忽略
}
setError((err as Error).message);
}
}
loadData();
// 清理函数:取消未完成的请求
return () => controller.abort();
}, []); // 依赖数组
useEffect(async () => { ... }, []) 会导致类型报错。正确做法是在 Effect 内部定义 async 函数再调用。
依赖数组与 ESLint
exhaustive-deps 规则帮你查漏补缺
依赖数组写错了,Effect 的行为就会和预期不同。ESLint 的 react-hooks/exhaustive-deps 规则能帮你检查。
// ESLint 配置
// .eslintrc.json
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/exhaustive-deps": "warn"
}
}
依赖缺失(危险)
// userId 变了,但 Effect 不会重新执行
useEffect(() => {
fetchUser(userId);
}, []); // 缺少 userId
依赖正确(安全)
// userId 变了,Effect 会重新执行
useEffect(() => {
fetchUser(userId);
}, [userId]); // 完整依赖
经验法则:Effect 里用到的所有外部变量都应该放进依赖数组。如果不想频繁触发,用 useMemo 或 useRef 缓存那个变量。
useCallback 类型
缓存回调函数,避免子组件不必要的重渲染
大多数情况下 TypeScript 能自动推断 useCallback 的类型。但如果回调的参数类型复杂,可以手动指定泛型。
// 自动推断——大多数情况够用
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// 手动指定——事件回调参数需要明确类型
const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(e) => {
setSearch(e.target.value); // e 的类型自动推断
},
[]
);
// 等价写法:直接在参数上标注类型
const handleSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
},
[]
);
React.memo 包裹的子组件)时,useCallback 才有意义。如果只在当前组件内使用,加了反而增加代码复杂度。
useMemo 类型
缓存计算结果,避免重复执行昂贵操作
// 自动推断——返回值类型从回调函数推断
const sortedList = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
); // 推断为 Item[]
// 手动指定——复杂返回值需要明确类型
const stats = useMemo<{ total: number; avg: number }>(() => {
const total = scores.reduce((sum, s) => sum + s, 0);
return { total, avg: total / scores.length };
}, [scores]);
// 过滤 + 转换的常见模式
const filteredUsers = useMemo(() => {
return users
.filter(u => u.age >= 18)
.map(u => ({ ...u, displayName: `${u.name} (${u.age})` }));
}, [users]); // 推断为 { id: string; name: string; age: number; displayName: string }[]
性能 vs 可读性:useMemo 不是免费的——它每次渲染都要对比依赖。如果计算本身很轻量(比如简单的 .map),用 useMemo 反而更慢。只在"计算昂贵"或"引用稳定"有实际意义时才用。
Part 3: 事件与表单
每个 DOM 事件都有对应的 React 类型。
表单是用户交互的核心。React 把原生 DOM 事件包装成了 SyntheticEvent,每个事件类型都有对应的泛型。
本节从事件类型讲起,延伸到完整表单的类型安全方案,包括受控组件和表单验证模式。
事件处理类型
React 自带一套完整的事件类型定义
function Form() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 表单提交逻辑
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value); // string
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log("clicked");
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") console.log("回车");
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onKeyDown={handleKeyDown} />
<button onClick={handleClick}>提交</button>
</form>
);
}
事件类型速查
六个最常用的事件类型
| 事件类型 | 触发场景 | 常用属性 |
|---|---|---|
FormEvent | 表单提交 | e.preventDefault() |
ChangeEvent | 输入变化 | e.target.value |
MouseEvent | 鼠标事件 | e.clientX/Y |
KeyboardEvent | 键盘事件 | e.key |
FocusEvent | 焦点事件 | e.target |
DragEvent | 拖拽事件 | e.dataTransfer |
受控表单类型
用类型约束表单状态,杜绝拼写错误
interface FormData {
username: string;
email: string;
age: number;
role: "admin" | "user" | "guest";
}
function TypedForm() {
const [form, setForm] = useState<FormData>({
username: "",
email: "",
age: 0,
role: "guest",
});
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
const updateField = <K extends keyof FormData>(
key: K,
value: FormData[K]
) => {
setForm(prev => ({ ...prev, [key]: value }));
setErrors(prev => ({ ...prev, [key]: undefined }));
};
const validate = (): boolean => {
const newErrors: typeof errors = {};
if (!form.username.trim()) newErrors.username = "用户名不能为空";
if (!form.email.includes("@")) newErrors.email = "邮箱格式错误";
if (form.age < 0 || form.age > 150) newErrors.age = "年龄不合理";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validate()) console.log("提交", form);
};
return (
<form onSubmit={handleSubmit}>
<input value={form.username}
onChange={e => updateField("username", e.target.value)} />
{errors.username && <span>{errors.username}</span>}
<input value={form.email}
onChange={e => updateField("email", e.target.value)} />
<input type="number" value={form.age}
onChange={e => updateField("age", Number(e.target.value))} />
<select value={form.role}
onChange={e => updateField("role", e.target.value as FormData["role"])}>
<option value="admin">管理员</option>
<option value="user">用户</option>
<option value="guest">访客</option>
</select>
<button type="submit">提交</button>
</form>
);
}
react-hook-form
社区最流行的表单方案,类型支持一流
受控表单在字段多时性能下降。react-hook-form 用非受控方式管理表单,减少不必要的渲染。
import { useForm } from "react-hook-form";
interface LoginForm {
email: string;
password: string;
remember: boolean;
}
function LoginForm() {
const { register, handleSubmit, formState: { errors } } =
useForm<LoginForm>({
defaultValues: { email: "", password: "", remember: false },
});
const onSubmit = (data: LoginForm) => {
console.log("登录", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email", {
required: "请输入邮箱",
pattern: { value: /\S+@\S+/, message: "邮箱格式错误" }
})} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register("password", {
required: "请输入密码",
minLength: { value: 6, message: "至少6位" }
})} />
{errors.password && <span>{errors.password.message}</span>}
<label>
<input type="checkbox" {...register("remember")} />
记住我
</label>
<button type="submit">登录</button>
</form>
);
}
useForm<LoginForm> 一行搞定泛型。register 返回的 ref、onChange 等属性会自动绑定到 input 上,字段名都有类型检查——拼错了立刻报红。
Part 4: Context 与泛型
跨组件传递数据,类型安全不能丢。
Context 解决了"逐层传 Props"的痛点,但它的默认值机制容易引入 null。用泛型封装后,消费者可以放心使用。
本节还会展示 Context + useReducer 的组合模式——这是 React 社区最常用的全局状态方案。
类型安全的 Context
三步走:定义类型、创建 Context、封装 Hook
// 1. 定义 Context 的数据类型
interface Theme {
mode: "light" | "dark";
toggle: () => void;
}
// 2. 创建 Context
const ThemeContext = createContext<Theme | null>(null);
// 3. 封装自定义 Hook(带空值检查)
function useTheme(): Theme {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
return ctx;
}
为什么要封装成 Hook?因为 useContext 返回的是 Theme | null,每次用都要判空。封装后返回 Theme,使用时不再需要判空。
Provider 与使用
Provider 提供数据,消费者通过 Hook 获取
// Provider 组件
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [mode, setMode] = useState<"light" | "dark">("light");
const toggle = () => setMode(m => m === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ mode, toggle }}>
{children}
</ThemeContext.Provider>
);
}
// 在任意子组件中使用——完整类型推断
function ThemedButton() {
const { mode, toggle } = useTheme();
return (
<button onClick={toggle}
style={{ background: mode === "dark" ? "#333" : "#fff" }}>
切换主题
</button>
);
}
Context + Reducer
用联合类型 Action 构建类型安全的全局状态
Context + useReducer 是 React 社区最常用的"轻量级状态管理"方案。类型写对了,dispatch 时编辑器会提示所有合法操作。
// 1. 定义 State 和 Action
interface Todo {
id: string;
text: string;
done: boolean;
}
type TodoAction =
| { type: "add"; text: string }
| { type: "toggle"; id: string }
| { type: "delete"; id: string }
| { type: "clear_completed" };
interface TodoState {
todos: Todo[];
nextId: number;
}
// 2. Reducer 函数
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case "add":
return {
...state,
todos: [...state.todos,
{ id: String(state.nextId), text: action.text, done: false }],
nextId: state.nextId + 1,
};
case "toggle":
return {
...state,
todos: state.todos.map(t =>
t.id === action.id ? { ...t, done: !t.done } : t
),
};
case "delete":
return { ...state, todos: state.todos.filter(t => t.id !== action.id) };
case "clear_completed":
return { ...state, todos: state.todos.filter(t => !t.done) };
}
}
全局状态封装
Provider 组件 + 自定义 Hook = 完美的类型安全
// 3. 创建 Context(值为 state + dispatch)
const TodoContext = createContext<{
state: TodoState;
dispatch: React.Dispatch<TodoAction>;
} | null>(null);
// 4. Provider 组件
function TodoProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
nextId: 1,
});
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
}
// 5. 自定义 Hook:空值保护
function useTodo() {
const ctx = useContext(TodoContext);
if (!ctx) throw new Error("useTodo must be inside TodoProvider");
return ctx;
}
// 6. 使用
function TodoList() {
const { state, dispatch } = useTodo();
return (
<ul>
{state.todos.map(t => (
<li key={t.id} onClick={() => dispatch({ type: "toggle", id: t.id })}
style={{ textDecoration: t.done ? "line-through" : "none" }}>
{t.text}
<button onClick={e => { e.stopPropagation(); dispatch({ type: "delete", id: t.id }); }}>
删除
</button>
</li>
))}
</ul>
);
}
这个模式的核心:useTodo() 返回的 dispatch 有完整类型约束——你只能传入合法的 Action 对象,字段名、类型全部有检查。
自定义 Hook 泛型
用泛型让 Hook 支持任意数据类型
// 泛型 Hook:封装数据获取逻辑
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
setData(json as T);
} catch (err) {
setError(err instanceof Error ? err.message : "未知错误");
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => { fetchData(); }, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// 使用——data 类型自动推断
interface User { id: string; name: string; }
const { data, loading, error } = useFetch<User[]>("/api/users");
// data: User[] | null
Part 5: ref 转发
让子组件暴露方法给父组件调用。
ref 默认只在原生 DOM 元素上有效。forwardRef 让你可以把 ref 传进自定义组件。
搭配 useImperativeHandle,你还能精确控制子组件暴露哪些方法,实现类型安全的命令式调用。
forwardRef
把 ref 从父组件"转发"进子组件
interface InputProps {
label: string;
placeholder?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, placeholder }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} placeholder={placeholder} />
</div>
)
);
forwardRef<HTMLInputElement, InputProps> 的两个泛型参数:第一个是 ref 指向的类型,第二个是 Props 类型。
ref 作为普通 prop,forwardRef 已被官方标记为"将来废弃"(仅老项目兼容用)。上面的等价现代写法:
// React 19+:ref 作为普通 prop 接收,无需 forwardRef
import { type Ref } from "react";
interface InputProps {
label: string;
placeholder?: string;
ref?: Ref<HTMLInputElement>; // 直接在 Props 里声明 ref
}
function Input({ label, placeholder, ref }: InputProps) {
return (
<div>
<label>{label}</label>
<input ref={ref} placeholder={placeholder} />
</div>
);
}
useImperativeHandle
自定义暴露给父组件的方法
// 子组件:定义暴露的接口
interface InputHandle {
focus: () => void;
clear: () => void;
getValue: () => string;
}
const ControlledInput = forwardRef<InputHandle, InputProps>(
({ label }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState("");
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => setValue(""),
getValue: () => value,
}));
return <input ref={inputRef} value={value}
onChange={e => setValue(e.target.value)} />;
}
);
// 父组件:通过 ref 调用子组件方法
function Parent() {
const inputRef = useRef<InputHandle>(null);
return (
<>
<ControlledInput ref={inputRef} label="姓名" />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
<button onClick={() => inputRef.current?.clear()}>清空</button>
</>
);
}
useImperativeHandle 配合 forwardRef 暴露自定义 handle 的模式仍然有效,但 React 19 同样支持把 ref 当普通 prop 接收(类型为 Ref<InputHandle>),省掉 forwardRef 包裹。新项目建议直接用 ref-as-prop 写法。
泛型组件
让一个组件支持多种数据类型
当你需要一个"通用列表"、"通用选择器"时,泛型组件就派上用场了。用 <T> 让组件适配任意数据类型。
// 泛型列表组件——渲染任意类型的数组
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={keyExtractor(item)}>{renderItem(item, i)}</li>
))}
</ul>
);
}
// 使用——类型自动推断
interface User { id: string; name: string; }
const users: User[] = [{ id: "1", name: "张三" }, { id: "2", name: "李四" }];
<List
items={users}
renderItem={user => <span>{user.name}</span>} // user 自动推断为 User
keyExtractor={user => user.id}
/>
泛型 Select
类型安全的下拉选择组件
// 泛型 Select:选项值可以是任意类型
interface SelectProps<T> {
options: { label: string; value: T }[];
value: T;
onChange: (value: T) => void;
}
function Select<T extends string | number>(
{ options, value, onChange }: SelectProps<T>
) {
return (
<select
value={String(value)}
onChange={e => {
const raw = e.target.value;
const selected = options.find(o => String(o.value) === raw);
if (selected) onChange(selected.value);
}}
>
{options.map(opt => (
<option key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</option>
))}
</select>
);
}
// 使用——value 和 onChange 的类型自动推断
type Role = "admin" | "user" | "guest";
const [role, setRole] = useState<Role>("guest");
<Select
options={[
{ label: "管理员", value: "admin" as Role },
{ label: "用户", value: "user" as Role },
{ label: "访客", value: "guest" as Role },
]}
value={role}
onChange={setRole} // setRole 的类型是 (value: Role) => void
/>
options 的 value 类型和 onChange 参数类型必须一致)。如果各 Props 独立,用联合类型就够了。
Part 6: 路由与性能
URL 参数和查询参数也需要类型安全。
路由参数默认是 string | undefined,查询参数永远是字符串——如果不加类型约束,很容易出 bug。
本节还补充了 React.memo 的类型要点:什么时候该用、什么时候不该用、自定义比较函数怎么写。
React Router 类型
useParams 和 useSearchParams
import { useParams, useSearchParams, Link } from "react-router-dom";
// useParams —— 获取路由参数
function UserProfile() {
const { userId } = useParams<{ userId: string }>();
if (!userId) return <div>用户ID缺失</div>;
return <div>用户: {userId}</div>;
}
// useSearchParams —— 获取查询参数
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const page = Number(searchParams.get("page")) || 1;
const goToPage = (p: number) => {
setSearchParams(prev => {
prev.set("page", String(p));
return prev;
});
};
return (
<div>
<p>第 {page} 页</p>
<button onClick={() => goToPage(page + 1)}>下一页</button>
</div>
);
}
React.memo
浅比较 Props,跳过不必要的渲染
当父组件重新渲染时,所有子组件默认也会重新渲染。React.memo 让子组件在 Props 没变时跳过渲染。
// 基本用法:Props 类型自动从泛型推断
interface ExpensiveListProps {
items: Item[];
onSelect: (id: string) => void;
}
const ExpensiveList = React.memo(function ExpensiveList(
{ items, onSelect }: ExpensiveListProps
) {
console.log("渲染 ExpensiveList");
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// 自定义比较函数
const StableList = React.memo(function StableList(
props: { data: ComplexData[] }
) {
return <div>{props.data.length} 条数据</div>
}, (prevProps, nextProps) => {
// 返回 true = 跳过渲染(Props 没变)
// 返回 false = 重新渲染(Props 变了)
return prevProps.data.length === nextProps.data.length
&& prevProps.data.every((d, i) => d.id === nextProps.data[i].id);
});
React.memo 做的是浅比较。如果每次渲染都传新的对象/数组/函数引用,memo 会失效。配合 useMemo 和 useCallback 才能真正起效。
常见排错
any 或 @ts-ignore 绕过类型检查。理解报错原因,写出真正类型安全的代码。
问题一:Props 解构时某个属性报"可能为 undefined"
原因:可选属性(带 ? 的)在 TS 中类型包含 undefined。
修复:使用前加判断,或用默认值:const { name = "默认名" } = props;
问题二:useRef 报"可能为 null"
原因:useRef<HTMLInputElement>(null) 的 current 类型是 HTMLInputElement | null。
修复:使用可选链 inputRef.current?.focus(),或在事件回调中确认非空后使用 ! 断言。
问题三:children 报类型错误
原因:没有声明 children 的类型。
修复:在 Props 接口中加上 children: React.ReactNode。
问题四:useEffect 里直接写 async 报错
原因:async 函数返回 Promise,而 useEffect 要求返回 void | (() => void)。
修复:在 Effect 内部定义 async 函数再调用,如 useEffect(() => { async function load() { ... } load(); }, [])。
问题五:泛型组件报"找不到名称 T"
原因:函数声明缺少泛型参数,或者用箭头函数时泛型写法不对。
修复:确保函数签名有 <T>,如 function List<T>(props: ListProps<T>)。箭头函数需要写成 <T,> 避免和 JSX 混淆。
问题六:React.memo 没起效,子组件还是频繁渲染
原因:每次渲染都传了新的对象/数组/函数引用,浅比较判定 Props 变了。
修复:用 useMemo 缓存对象和数组,用 useCallback 缓存回调函数,确保稳定引用。
试一试
练习一
任务:创建一个 TodoItem 组件,Props 包含 title(string)、done(boolean,默认 false)、onToggle(回调函数)。点击时切换完成状态。
// 提示:定义 interface TodoItemProps
// 你的代码:
参考答案
interface TodoItemProps {
title: string;
done?: boolean;
onToggle: () => void;
}
function TodoItem({ title, done = false, onToggle }: TodoItemProps) {
return (
<div onClick={onToggle}
style={{ textDecoration: done ? "line-through" : "none" }}>
{title}
</div>
);
}
练习二
任务:用 useReducer 实现一个计数器,支持三种操作:加 1、减 1、重置为 0。确保 dispatch 的类型安全。
// 提示:定义 Action 联合类型
// 你的代码:
参考答案
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + 1;
case "decrement": return state - 1;
case "reset": return 0;
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<>
<p>{count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
<button onClick={() => dispatch({ type: "decrement" })}>-1</button>
<button onClick={() => dispatch({ type: "reset" })}>重置</button>
</>
);
}
练习三
任务:创建一个 Context + useReducer 的主题管理器。State 包含 theme("light" | "dark" | "system")和 fontSize(number)。Action 支持切换主题和调整字号。封装 useThemeContext() Hook,在 Provider 外使用时抛出错误。
// 提示:定义 ThemeState 和 ThemeAction
// 封装 ThemeProvider 和 useThemeContext
// 你的代码:
参考答案
interface ThemeState {
theme: "light" | "dark" | "system";
fontSize: number;
}
type ThemeAction =
| { type: "set_theme"; theme: ThemeState["theme"] }
| { type: "set_fontSize"; size: number };
function themeReducer(state: ThemeState, action: ThemeAction): ThemeState {
switch (action.type) {
case "set_theme": return { ...state, theme: action.theme };
case "set_fontSize": return { ...state, fontSize: action.size };
}
}
const ThemeCtx = createContext<{ state: ThemeState; dispatch: React.Dispatch<ThemeAction> } | null>(null);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(themeReducer, {
theme: "system", fontSize: 14,
});
return <ThemeCtx.Provider value={{ state, dispatch }}>{children}</ThemeCtx.Provider>;
}
function useThemeContext() {
const ctx = useContext(ThemeCtx);
if (!ctx) throw new Error("useThemeContext must be inside ThemeProvider");
return ctx;
}
📝 要点总结
本章核心知识点
组件类型
Props 接口定义、children 类型、FC 函数组件类型
Hook 类型
useState 泛型、useRef 可变/不可变、useReducer 完整类型
事件与表单
ChangeEvent、MouseEvent 等事件类型,受控表单类型
Context 与 Ref
createContext 泛型、forwardRef + useImperativeHandle
高级模式
泛型组件、React Router 类型、react-hook-form 集成
下一章
React + TS 搞定了。下一章学习 Vue 3 与 TypeScript——Composition API 下的类型写法,同样优雅。