React 完全指南
相关文档:TypeScript完全指南(/typescript-wan-quan-zhi-nan/) TanStack Query 完全指南(/tanstack-query-wan-quan-zhi-nan/) TanStack Router 完全指南(/tanstack-router-wan-quan-zhi-nan/) React 是 Meta 开发的用于构建用户界面的 JavaScript 库,核心思想是声明式(描述 UI 应该是什么样子,而不是如何操作 DOM)和组件化(将 UI 拆分为可复用的独立部分)。 JSX 是 JavaScript 的语
官方文档:https://react.dev/
适用版本:React 19(2026-05-07 核实)
相关文档:TypeScript完全指南 TanStack Query 完全指南 TanStack Router 完全指南
1. 基础概念
React 是什么
React 是 Meta 开发的用于构建用户界面的 JavaScript 库,核心思想是声明式(描述 UI 应该是什么样子,而不是如何操作 DOM)和组件化(将 UI 拆分为可复用的独立部分)。
React vs Vue3
| 特性 | React | Vue 3 |
|---|---|---|
| 范式 | 函数式、JSX | 模板语法(可选 JSX) |
| 状态管理 | useState + useReducer | ref + reactive |
| 响应式 | 手动触发(setState) | 自动追踪 |
| 生态 | 极其丰富 | 丰富 |
| 学习曲线 | 中等(JSX 思维转变) | 低(模板更直观) |
| TypeScript | 天然友好 | 良好 |
创建项目
# Vite(推荐)
npm create vite@latest my-app -- --template react-ts
cd my-app && npm install && npm run dev
# Next.js(SSR/SSG 首选)
npx create-next-app@latest my-app --typescript --tailwind
2. JSX
JSX 是 JavaScript 的语法扩展,允许在 JS 中写类 HTML 的标记:
function Greeting({ name }: { name: string }) {
const isLoggedIn = true;
const items = ["Apple", "Banana", "Cherry"];
return (
<div className="container"> {/* class 写成 className */}
<h1>Hello, {name}!</h1> {/* {} 内写 JS 表达式 */}
{/* 条件渲染 */}
{isLoggedIn ? <span>已登录</span> : <span>未登录</span>}
{isLoggedIn && <button>退出</button>}
{/* 列表渲染(key 必须是唯一稳定的值) */}
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
{/* 内联样式 */}
<p style={{ color: "red", fontSize: 16 }}>警告信息</p>
</div>
);
}
3. 组件
函数组件
// 最简单的函数组件
function Button({ label, onClick }: { label: string; onClick: () => void }) {
return <button onClick={onClick}>{label}</button>;
}
// 带 children 的组件
function Card({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
// 使用
<Card title="用户信息">
<p>这是卡片内容</p>
</Card>
Props 默认值
interface ButtonProps {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
onClick?: () => void;
}
function Button({ label, variant = "primary", disabled = false, onClick }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
);
}
4. Hooks
useState — 状态
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const [user, setUser] = useState<{ name: string; age: number } | null>(null);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* 函数更新(依赖旧值时推荐) */}
<button onClick={() => setCount(prev => prev + 1)}>安全 +1</button>
<button onClick={() => setUser({ name: "Alice", age: 25 })}>设置用户</button>
{/* 更新对象:必须展开旧值 */}
<button onClick={() => setUser(prev => prev ? { ...prev, age: 26 } : null)}>
更新年龄
</button>
</div>
);
}
useEffect — 副作用
import { useEffect, useState } from "react";
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 副作用:数据获取(建议用 TanStack Query 替代)
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => {
if (!cancelled) setUser(data);
});
// 清理函数:组件卸载或 userId 变化时执行
return () => { cancelled = true; };
}, [userId]); // 依赖数组:userId 变化时重新执行
// 依赖数组为 []:只在组件挂载时执行一次
useEffect(() => {
document.title = "用户信息";
return () => { document.title = "My App"; };
}, []);
return <div>{user?.name}</div>;
}
useRef — 引用
import { useRef, useEffect } from "react";
function VideoPlayer() {
// DOM 引用
const videoRef = useRef<HTMLVideoElement>(null);
// 普通值引用(不触发重渲染,适合存定时器 ID 等)
const timerRef = useRef<number | null>(null);
useEffect(() => {
videoRef.current?.play();
timerRef.current = setInterval(() => {
console.log("tick");
}, 1000);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, []);
return <video ref={videoRef} src="/video.mp4" />;
}
useMemo — 缓存计算结果
import { useMemo } from "react";
function ProductList({ products, keyword }: { products: Product[]; keyword: string }) {
// 只有 products 或 keyword 变化时才重新过滤
const filtered = useMemo(
() => products.filter(p => p.name.includes(keyword)),
[products, keyword]
);
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
useCallback — 缓存函数引用
import { useCallback } from "react";
function Parent() {
const [count, setCount] = useState(0);
// 如果不用 useCallback,每次 Parent 重渲染都会创建新函数
// 导致传入 Child 的 onClick prop 变化,Child 也会重渲染
const handleClick = useCallback(() => {
console.log("clicked");
}, []); // 空依赖:函数永远不变
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child onClick={handleClick} />
</div>
);
}
// 只有 onClick 变化时才重渲染
const Child = React.memo(({ onClick }: { onClick: () => void }) => {
console.log("Child rendered");
return <button onClick={onClick}>子按钮</button>;
});
useReducer — 复杂状态管理
import { useReducer } from "react";
type State = { count: number; step: number };
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" }
| { type: "setStep"; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment": return { ...state, count: state.count + state.step };
case "decrement": return { ...state, count: state.count - state.step };
case "reset": return { ...state, count: 0 };
case "setStep": return { ...state, step: action.payload };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
return (
<div>
<p>count: {state.count}, step: {state.step}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "setStep", payload: 5 })}>步长=5</button>
</div>
);
}
useContext — 跨组件共享状态
import { createContext, useContext, useState } from "react";
interface AuthContextType {
user: User | null;
login: (user: User) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
// Provider
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
return (
<AuthContext.Provider value={{
user,
login: (u) => setUser(u),
logout: () => setUser(null),
}}>
{children}
</AuthContext.Provider>
);
}
// 自定义 Hook(封装 useContext,提供类型安全)
function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}
// 使用
function Header() {
const { user, logout } = useAuth();
return <div>{user ? <button onClick={logout}>退出</button> : "未登录"}</div>;
}
5. 自定义 Hook
自定义 Hook 是以 use 开头的函数,封装可复用的状态逻辑:
// useLocalStorage
import { useState } from "react";
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = (newValue: T) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
};
return [value, setStoredValue] as const;
}
// useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// 使用
function SearchInput() {
const [keyword, setKeyword] = useState("");
const debouncedKeyword = useDebounce(keyword, 300);
// 用 debouncedKeyword 发起请求,避免每次输入都请求
const { data } = useQuery({
queryKey: ["search", debouncedKeyword],
queryFn: () => search(debouncedKeyword),
enabled: debouncedKeyword.length > 0,
});
}
6. React.memo 与性能优化
// React.memo:props 不变时跳过重渲染
const ExpensiveComponent = React.memo(({ data }: { data: string[] }) => {
return <ul>{data.map(d => <li key={d}>{d}</li>)}</ul>;
});
// 自定义比较函数
const Component = React.memo(MyComponent, (prevProps, nextProps) => {
return prevProps.id === nextProps.id; // 返回 true 则不重渲染
});
7. 常用代码段
受控表单
function LoginForm() {
const [form, setForm] = useState({ email: "", password: "" });
const [error, setError] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await login(form);
} catch (err) {
setError("登录失败");
}
};
return (
<form onSubmit={handleSubmit}>
<input name="email" value={form.email} onChange={handleChange} />
<input name="password" type="password" value={form.password} onChange={handleChange} />
{error && <p>{error}</p>}
<button type="submit">登录</button>
</form>
);
}
错误边界(Error Boundary)
import { Component, ErrorInfo, ReactNode } from "react";
class ErrorBoundary extends Component<
{ children: ReactNode; fallback?: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error(error, info);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <div>出错了,请刷新页面</div>;
}
return this.props.children;
}
}
// 使用
<ErrorBoundary fallback={<div>加载失败</div>}>
<UserList />
</ErrorBoundary>
8. 最佳实践
状态提升
当多个组件需要共享状态时,将状态提升到最近的公共父组件。避免过度使用 Context(Context 变化会导致所有消费者重渲染)。
服务端数据用 TanStack Query,客户端状态用 useState
// 服务端数据(远程状态)→ TanStack Query
const { data: users } = useQuery({ queryKey: ["users"], queryFn: fetchUsers });
// 客户端状态(UI 状态)→ useState
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedId, setSelectedId] = useState<number | null>(null);
避免 useEffect 滥用
useEffect 常见的错误用法:
// 错误:用 useEffect 同步状态(应该直接在渲染中计算)
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// 正确:直接计算
const fullName = `${firstName} ${lastName}`;
9. 踩坑与注意事项
不能在条件/循环中使用 Hook
// 错误
if (condition) {
const [state, setState] = useState(0); // 违反 Hook 规则
}
// 正确:Hook 必须在组件顶层无条件调用
const [state, setState] = useState(0);
if (condition) { /* 使用 state */ }
state 更新是异步的,不能立即读取新值
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 还是旧值!React 批量处理更新
};
// 如果下次更新依赖当前值,用函数式更新
setCount(prev => prev + 1);
列表的 key 不要用 index
// 不推荐:key=index 在列表顺序变化时会导致状态错乱
{items.map((item, index) => <Item key={index} {...item} />)}
// 推荐:使用稳定的唯一 ID
{items.map((item) => <Item key={item.id} {...item} />)}
---
## 最佳实践
**组件保持小而专注(Single Responsibility)**:一个组件只做一件事。超过 200 行的组件通常需要拆分,提取独立的 hook 或子组件。大组件难以测试、复用和理解。
**用自定义 Hook 封装可复用逻辑**:将 `useState + useEffect + 业务逻辑` 提取为 `useXxx` 函数,组件只关心渲染,逻辑可在多个组件间复用和单独测试。
```tsx
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight })
useEffect(() => {
const handler = () => setSize({ width: window.innerWidth, height: window.innerHeight })
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
return size
}
useEffect 依赖数组要完整,且清理副作用:缺少依赖会导致闭包过期(stale closure);缺少清理函数会导致组件卸载后定时器 / 事件监听仍在运行,产生内存泄漏。
useEffect(() => {
const timer = setInterval(() => setCount(c => c + 1), 1000)
return () => clearInterval(timer) // 清理函数
}, []) // 空依赖 = 只执行一次
服务端状态用 TanStack Query,客户端 UI 状态用 useState:网络请求数据(loading / error / data / cache / refetch)是服务端状态,用 TanStack Query 管理比手写 useState + useEffect 少写 80% 代码,且自动缓存、去重、后台刷新。
避免在渲染函数中直接修改 state:在 JSX 渲染期间调用 setState 会导致无限渲染循环。所有 state 更新放在事件处理函数或 useEffect 中。
常见陷阱
陷阱:列表 key 用 index 导致状态错乱
现象: 删除列表中间的某项后,其他项的输入框内容错位、复选框勾选状态混乱。
原因: React 用 key 识别哪些元素发生了变化。用 index 作为 key 时,删除第 2 项后,原来第 3 项的 key 变成了 2(与原第 2 项相同),React 认为是同一元素,状态不重置。
解决: 始终用数据的唯一标识符(id、uuid)作为 key。
// 错误
{items.map((item, i) => <Input key={i} value={item.name} />)}
// 正确
{items.map(item => <Input key={item.id} value={item.name} />)}
陷阱:闭包过期(Stale Closure)
现象: 在 useEffect 中的定时器或事件回调中读取 state,始终是初始值(如 0),不是最新值。
原因: 函数在创建时捕获了当时的变量值(闭包),即使 state 已更新,旧的函数仍引用旧值。
解决: 在 useEffect 依赖数组中加入 state 变量,或使用函数式更新 setCount(prev => prev + 1)。
// 问题:count 永远是 0
useEffect(() => {
setInterval(() => console.log(count), 1000)
}, [])
// 正确:用函数式更新
useEffect(() => {
setInterval(() => setCount(c => c + 1), 1000)
return () => clearInterval(timer)
}, [])
陷阱:useEffect 缺少清理导致内存泄漏
现象: 组件卸载后控制台报 Can't perform a React state update on an unmounted component,或事件监听在组件销毁后仍触发。
原因: useEffect 中添加了事件监听、定时器、WebSocket 等副作用,但没有返回清理函数,组件卸载后这些副作用仍在运行。
解决: 所有 useEffect 中添加的副作用必须在返回函数中清理。
useEffect(() => {
const ws = new WebSocket(url)
ws.onmessage = (e) => setMessages(m => [...m, e.data])
return () => ws.close() // 清理:组件卸载时关闭 WebSocket
}, [url])
参见