
React Expert
- 12 installs
- 1 repo stars
- Updated July 28, 2026
- evanfang0054/cc-system-creator-scripts
Helps with frontend development tasks.
About
react-expert is a Claude Code skill for frontend development. It helps developers move faster with AI-assisted coding.
- react-expert
- Frontend Development
- AI-coding skill
React Expert by the numbers
- 12 all-time installs (skills.sh)
- Ranked #1,643 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/evanfang0054/cc-system-creator-scripts --skill react-expertAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 28, 2026 |
| Repository | evanfang0054/cc-system-creator-scripts ↗ |
What it does
Helps with frontend development tasks.
Files
React 专家
资深 React 专家,在 React 19、Server Components 和生产级应用架构方面具有深厚的专业知识。
角色定义
你是一位拥有 10+ 年前端经验的资深 React 工程师。你专注于 React 19 模式,包括 Server Components、use() hook 和 form actions。你使用 TypeScript 和现代状态管理构建可访问、高性能的应用程序。
何时使用此技能
- 构建新的 React 组件或功能
- 实现状态管理(本地、Context、Redux、Zustand)
- 优化 React 性能
- 设置 React 项目架构
- 使用 React 19 Server Components
- 使用 React 19 actions 实现表单
- 使用 TanStack Query 或
use()进行数据获取模式
核心工作流程
1. 分析需求 - 识别组件层次结构、状态需求、数据流 2. 选择模式 - 选择合适的状态管理、数据获取方法 3. 实现 - 编写具有正确类型的 TypeScript 组件 4. 优化 - 在需要的地方应用 memoization,确保可访问性 5. 测试 - 使用 React Testing Library 编写测试
参考指南
根据上下文加载详细指导:
| 主题 | 参考 | 加载时机 |
|---|---|---|
| Server Components | references/server-components.md | RSC 模式、Next.js App Router |
| React 19 | references/react-19-features.md | use() hook、useActionState、表单 |
| State Management | references/state-management.md | Context、Zustand、Redux、TanStack |
| Hooks | references/hooks-patterns.md | 自定义 hooks、useEffect、useCallback |
| Performance | references/performance.md | memo、lazy、虚拟化 |
| Testing | references/testing-react.md | Testing Library、mock |
| Class Migration | references/migration-class-to-modern.md | 将类组件转换为 hooks/RSC |
约束条件
必须做
- 使用 TypeScript 的严格模式
- 实现错误边界以优雅地处理失败
- 正确使用
keyprops(稳定的、唯一标识符) - 清理 effects(返回清理函数)
- 使用语义化 HTML 和 ARIA 确保可访问性
- 向 memoized 的子组件传递回调/对象时进行 memoization
- 对异步操作使用 Suspense 边界
禁止做
- 直接修改 state
- 对动态列表使用数组索引作为 key
- 在 JSX 内部创建函数(导致重新渲染)
- 忘记 useEffect 清理(内存泄漏)
- 忽略 React 严格模式警告
- 在生产环境中跳过错误边界
输出模板
实现 React 功能时,提供: 1. 带有 TypeScript 类型的组件文件 2. 如果有非平凡逻辑,提供测试文件 3. 关键决策的简要说明
知识参考
React 19、Server Components、use() hook、Suspense、TypeScript、TanStack Query、Zustand、Redux Toolkit、React Router、React Testing Library、Vitest/Jest、Next.js App Router、可访问性(WCAG)
相关技能
- Fullstack Guardian - 全栈功能实现
- Playwright Expert - React 应用的 E2E 测试
- Test Master - 综合测试策略
Hooks 模式
自定义 Hook 模式
// useApi - 数据获取 hook
function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, error, loading };
}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 Search() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) search(debouncedQuery);
}, [debouncedQuery]);
}useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}useMediaQuery
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() =>
typeof window !== 'undefined' && window.matchMedia(query).matches
);
useEffect(() => {
const media = window.matchMedia(query);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);
return matches;
}
// 使用示例
function Layout() {
const isMobile = useMediaQuery('(max-width: 768px)');
return isMobile ? <MobileNav /> : <DesktopNav />;
}useCallback & useMemo
// useCallback: 记忆化函数(用于子组件依赖)
const handleClick = useCallback((id: string) => {
setSelected(id);
}, []); // 空依赖 = 稳定引用
// useMemo: 记忆化昂贵计算
const sortedItems = useMemo(() =>
[...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// 何时使用:
// - useCallback: 向 memoized 的子组件传递时
// - useMemo: 计算昂贵且依赖很少变化时Effect 清理
useEffect(() => {
const subscription = api.subscribe(handler);
// 清理函数
return () => subscription.unsubscribe();
}, []);
// 异步 effect 模式
useEffect(() => {
let cancelled = false;
async function fetchData() {
const data = await api.getData();
if (!cancelled) setData(data);
}
fetchData();
return () => { cancelled = true };
}, []);快速参考
| Hook | 用途 |
|---|---|
| useState | 组件状态 |
| useEffect | 副作用、订阅 |
| useCallback | 记忆化函数 |
| useMemo | 记忆化值 |
| useRef | 可变 ref、DOM 访问 |
| useContext | 读取 context |
| useReducer | 复杂状态逻辑 |
| 自定义 Hook | 使用场景 |
|---|---|
| useDebounce | 输入延迟 |
| useLocalStorage | 持久化状态 |
| useMediaQuery | 响应式逻辑 |
| useApi | 数据获取 |
从类组件到现代 React 迁移指南
---
何时使用此指南
应该迁移时:
- 采用 React 18+ 特性(并发渲染、Suspense)
- 提高代码可重用性和组合性
- 减小打包体积(hooks 通常更小)
- 在 Next.js 13+ 中启用 Server Components
- 团队标准化到现代模式
- 存在性能优化机会
- 需要降低测试复杂度
不应迁移时:
- 错误边界(仍需要类组件)
- 没有维护预算的遗留代码库
- 组件完美运行且不会变化
- 团队缺乏 hooks 专业知识
- 第三方库需要类继承
- 迁移风险超过收益
迁移优先级: 1. 新功能(使用 hooks 编写) 2. 频繁修改的组件 3. 具有可重用逻辑的组件 4. 性能瓶颈 5. 稳定、工作的组件(最低优先级)
---
生命周期到 Hooks 概念映射
| 类组件 | 现代 React 等价物 | 说明 |
|---|---|---|
constructor | useState 初始化 | 不需要单独的构造函数 |
componentDidMount | useEffect(() => {}, []) | 空依赖数组 |
componentDidUpdate | useEffect(() => {}) | 每次渲染后运行 |
componentWillUnmount | useEffect 清理 | 返回清理函数 |
shouldComponentUpdate | React.memo | 包装组件,自定义比较器 |
getDerivedStateFromProps | 避免或使用渲染时计算 | 通常是反模式 |
getSnapshotBeforeUpdate | useLayoutEffect | 很少需要 |
componentDidCatch | 无 hook 等价物 | 保留类组件 |
this.forceUpdate() | useState + setter 切换 | 避免,修复架构 |
this.state | useState 或 useReducer | 多个状态片段 |
this.setState 回调 | useEffect 监听状态 | 单独的 effect |
---
模式 1: 构造函数和状态 → useState
类组件
interface Props {
initialCount: number;
userId: string;
}
interface State {
count: number;
user: User | null;
isLoading: boolean;
}
class Counter extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
count: props.initialCount,
user: null,
isLoading: false,
};
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}现代 React
interface Props {
initialCount: number;
userId: string;
}
interface User {
id: string;
name: string;
}
function Counter({ initialCount, userId }: Props) {
// 分离状态片段以获得更好的粒度
const [count, setCount] = useState(initialCount);
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
// 箭头函数不再需要绑定
const increment = () => {
setCount(prev => prev + 1); // 函数式更新确保安全
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}关键差异:
- 不需要构造函数
- 懒初始化:
useState(() => expensiveComputation()) - 函数式更新避免闭包过期 bug
- 分离的
useState调用改善重新渲染优化
---
模式 2: 生命周期方法 → useEffect
类组件
class UserProfile extends React.Component<{ userId: string }, State> {
state = {
user: null as User | null,
posts: [] as Post[],
};
async componentDidMount() {
await this.fetchUser();
await this.fetchPosts();
window.addEventListener('resize', this.handleResize);
}
async componentDidUpdate(prevProps: Props) {
if (prevProps.userId !== this.props.userId) {
await this.fetchUser();
await this.fetchPosts();
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
fetchUser = async () => {
const user = await api.getUser(this.props.userId);
this.setState({ user });
};
fetchPosts = async () => {
const posts = await api.getPosts(this.props.userId);
this.setState({ posts });
};
handleResize = () => {
// 处理 resize
};
render() {
return <div>{this.state.user?.name}</div>;
}
}现代 React
interface Props {
userId: string;
}
interface User {
id: string;
name: string;
}
interface Post {
id: string;
title: string;
}
function UserProfile({ userId }: Props) {
const [user, setUser] = useState<User | null>(null);
const [posts, setPosts] = useState<Post[]>([]);
// userId 变化时获取用户
useEffect(() => {
let cancelled = false;
async function fetchUser() {
const userData = await api.getUser(userId);
if (!cancelled) {
setUser(userData);
}
}
fetchUser();
// 清理以防止卸载后状态更新
return () => {
cancelled = true;
};
}, [userId]); // userId 变化时重新运行
// userId 变化时获取帖子
useEffect(() => {
let cancelled = false;
async function fetchPosts() {
const postsData = await api.getPosts(userId);
if (!cancelled) {
setPosts(postsData);
}
}
fetchPosts();
return () => {
cancelled = true;
};
}, [userId]);
// 带清理的事件监听器
useEffect(() => {
function handleResize() {
// 处理 resize
}
window.addEventListener('resize', handleResize);
// 清理移除监听器
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 空数组 = 仅挂载/卸载
return <div>{user?.name}</div>;
}关键点:
- 为不同关注点分离 effects
- 始终为订阅添加清理
- 取消标志防止内存泄漏
- 依赖数组必须包含所有使用的值
- 空数组
[]= 仅挂载/卸载 - 无数组 = 每次渲染后(很少需要)
---
模式 3: shouldComponentUpdate → React.memo
类组件
class ExpensiveList extends React.Component<Props> {
shouldComponentUpdate(nextProps: Props) {
return (
nextProps.items !== this.props.items ||
nextProps.filter !== this.props.filter
);
}
render() {
const { items, filter } = this.props;
const filtered = items.filter(item => item.includes(filter));
return (
<ul>
{filtered.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
}
}现代 React
interface Props {
items: string[];
filter: string;
onItemClick?: (item: string) => void;
}
// React.memo 带自定义比较
const ExpensiveList = React.memo<Props>(
({ items, filter, onItemClick }) => {
// useMemo 用于昂贵计算
const filtered = useMemo(
() => items.filter(item => item.includes(filter)),
[items, filter]
);
return (
<ul>
{filtered.map(item => (
<li key={item} onClick={() => onItemClick?.(item)}>
{item}
</li>
))}
</ul>
);
},
// 自定义比较函数(可选)
(prevProps, nextProps) => {
return (
prevProps.items === nextProps.items &&
prevProps.filter === nextProps.filter &&
prevProps.onItemClick === nextProps.onItemClick
);
}
);
ExpensiveList.displayName = 'ExpensiveList';优化清单:
React.memo防止 props 未变化时重新渲染useMemo缓存昂贵计算useCallback稳定函数引用- 自定义比较器用于复杂 props
- 默认使用浅比较
---
模式 4: 复杂状态 → useReducer
类组件
class TodoManager extends React.Component<{}, State> {
state = {
todos: [] as Todo[],
filter: 'all' as Filter,
editingId: null as string | null,
};
addTodo = (text: string) => {
this.setState(prev => ({
todos: [...prev.todos, { id: uuid(), text, completed: false }],
}));
};
toggleTodo = (id: string) => {
this.setState(prev => ({
todos: prev.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
}));
};
deleteTodo = (id: string) => {
this.setState(prev => ({
todos: prev.todos.filter(todo => todo.id !== id),
}));
};
setFilter = (filter: Filter) => {
this.setState({ filter });
};
}现代 React
interface Todo {
id: string;
text: string;
completed: boolean;
}
type Filter = 'all' | 'active' | 'completed';
interface State {
todos: Todo[];
filter: Filter;
editingId: string | null;
}
type Action =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: string }
| { type: 'DELETE_TODO'; id: string }
| { type: 'SET_FILTER'; filter: Filter }
| { type: 'START_EDITING'; id: string }
| { type: 'STOP_EDITING' };
function todoReducer(state: State, action: Action): State {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{ id: crypto.randomUUID(), text: action.text, completed: false },
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.id),
};
case 'SET_FILTER':
return { ...state, filter: action.filter };
case 'START_EDITING':
return { ...state, editingId: action.id };
case 'STOP_EDITING':
return { ...state, editingId: null };
default:
return state;
}
}
function TodoManager() {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all',
editingId: null,
});
// Action creators
const addTodo = (text: string) => {
dispatch({ type: 'ADD_TODO', text });
};
const toggleTodo = (id: string) => {
dispatch({ type: 'TOGGLE_TODO', id });
};
// 使用 useMemo 派生状态
const visibleTodos = useMemo(() => {
switch (state.filter) {
case 'active':
return state.todos.filter(t => !t.completed);
case 'completed':
return state.todos.filter(t => t.completed);
default:
return state.todos;
}
}, [state.todos, state.filter]);
return (
<div>
{visibleTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => toggleTodo(todo.id)}
/>
))}
</div>
);
}何时使用 useReducer:
- 多个相关的状态值
- 复杂的状态转换
- 下一个状态依赖于前一个
- 单独测试状态逻辑
- 需要 Redux 式的可预测性
---
模式 5: Refs 迁移
类组件
class FormWithFocus extends React.Component {
inputRef = React.createRef<HTMLInputElement>();
timeoutId: number | null = null;
componentDidMount() {
this.inputRef.current?.focus();
}
componentWillUnmount() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
handleSubmit = () => {
const value = this.inputRef.current?.value;
console.log(value);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input ref={this.inputRef} />
</form>
);
}
}现代 React
function FormWithFocus() {
// DOM ref
const inputRef = useRef<HTMLInputElement>(null);
// 可变值 ref(跨渲染持久化)
const timeoutIdRef = useRef<number | null>(null);
useEffect(() => {
// 挂载时聚焦
inputRef.current?.focus();
// 卸载时清理 timeout
return () => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
};
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const value = inputRef.current?.value;
console.log(value);
};
const handleDelayedAction = () => {
timeoutIdRef.current = window.setTimeout(() => {
console.log('Delayed action');
}, 1000);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
<button type="button" onClick={handleDelayedAction}>
Delayed
</button>
</form>
);
}Ref 使用场景:
- DOM 访问(focus、scroll、测量)
- 存储可变值(timers、订阅)
- 跟踪之前的值
- 实例变量替换
---
模式 6: HOC → 自定义 Hooks
带 HOC 的类组件
// HOC
function withAuth<P extends object>(
Component: React.ComponentType<P & { user: User }>
) {
return class extends React.Component<P> {
state = { user: null as User | null };
componentDidMount() {
this.fetchUser();
}
fetchUser = async () => {
const user = await auth.getCurrentUser();
this.setState({ user });
};
render() {
if (!this.state.user) return <div>Loading...</div>;
return <Component {...this.props} user={this.state.user} />;
}
};
}
// 使用
class Dashboard extends React.Component<{ user: User }> {
render() {
return <div>Welcome {this.props.user.name}</div>;
}
}
export default withAuth(Dashboard);带自定义 Hook 的现代 React
// 自定义 hook
function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
const userData = await auth.getCurrentUser();
if (!cancelled) {
setUser(userData);
setLoading(false);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err : new Error('Auth failed'));
setLoading(false);
}
}
}
fetchUser();
return () => {
cancelled = true;
};
}, []);
const logout = useCallback(async () => {
await auth.logout();
setUser(null);
}, []);
return { user, loading, error, logout };
}
// 使用
function Dashboard() {
const { user, loading, error, logout } = useAuth();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>Not authenticated</div>;
return (
<div>
<p>Welcome {user.name}</p>
<button onClick={logout}>Logout</button>
</div>
);
}自定义 Hook 优势:
- 更容易组合(使用多个 hooks)
- 更好的 TypeScript 推断
- 无包装组件(更简单的树)
- 更容易单独测试
- 更明确的依赖
---
模式 7: Render Props → 自定义 Hooks
带 Render Props 的类组件
interface MousePosition {
x: number;
y: number;
}
class Mouse extends React.Component<
{ children: (pos: MousePosition) => React.ReactNode },
MousePosition
> {
state = { x: 0, y: 0 };
handleMouseMove = (e: MouseEvent) => {
this.setState({ x: e.clientX, y: e.clientY });
};
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove);
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove);
}
render() {
return this.props.children(this.state);
}
}
// 使用
<Mouse>
{({ x, y }) => (
<div>
Mouse at {x}, {y}
</div>
)}
</Mouse>带自定义 Hook 的现代 React
interface MousePosition {
x: number;
y: number;
}
function useMouse(): MousePosition {
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
useEffect(() => {
function handleMouseMove(e: MouseEvent) {
setPosition({ x: e.clientX, y: e.clientY });
}
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return position;
}
// 使用
function MouseTracker() {
const { x, y } = useMouse();
return (
<div>
Mouse at {x}, {y}
</div>
);
}Hook 优势:
- 无额外嵌套
- 更清晰的数据流
- 轻松组合多个 hooks
- 更好的性能(无包装渲染)
---
模式 8: Context 迁移
类组件
const ThemeContext = React.createContext<Theme>('light');
class ThemedButton extends React.Component {
static contextType = ThemeContext;
declare context: React.ContextType<typeof ThemeContext>;
render() {
return <button className={this.context}>{this.props.children}</button>;
}
}
// 或使用 Consumer
class ThemedButton2 extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{theme => <button className={theme}>{this.props.children}</button>}
</ThemeContext.Consumer>
);
}
}现代 React
type Theme = 'light' | 'dark';
interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = React.createContext<ThemeContextValue | undefined>(
undefined
);
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
const toggleTheme = useCallback(() => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
}, []);
const value = useMemo(
() => ({ theme, toggleTheme }),
[theme, toggleTheme]
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
// 使用
function ThemedButton({ children }: { children: React.ReactNode }) {
const { theme, toggleTheme } = useTheme();
return (
<button className={theme} onClick={toggleTheme}>
{children}
</button>
);
}Context 最佳实践:
- 自定义 hook 用于消费 context
- 记忆化 context 值以防止重新渲染
- 按更新频率分割 contexts
- 提供类型安全并检查 undefined
---
Server Components 迁移
现代 Next.js 13+ 支持 Server Components,它们不能使用 hooks。
Client Component (Hooks)
'use client';
import { useState, useEffect } from 'react';
export function ClientCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Client-side effect');
}, []);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Server Component (Async)
// app/page.tsx - 默认为 Server Component
interface User {
id: string;
name: string;
}
async function getUser(id: string): Promise<User> {
const res = await fetch(`https://api.example.com/users/${id}`, {
next: { revalidate: 3600 }, // 缓存 1 小时
});
return res.json();
}
export default async function UserProfile({ params }: { params: { id: string } }) {
const user = await getUser(params.id);
return (
<div>
<h1>{user.name}</h1>
{/* Client component 用于交互 */}
<ClientCounter />
</div>
);
}Server vs Client 决策树:
- 需要交互(onClick、state)? → Client Component
- 需要浏览器 API(localStorage、window)? → Client Component
- 需要 effects 或 hooks? → Client Component
- 获取数据、读取文件、数据库? → Server Component
- SEO 关键内容? → Server Component
- 大型依赖? → Server Component(更小的客户端打包)
参考:react-expert/references/server-components.md
---
常见陷阱
1. 闭包过期
问题:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 总是打印 0!
setCount(count + 1); // 总是设置为 1!
}, 1000);
return () => clearInterval(id);
}, []); // 缺少依赖
return <div>{count}</div>;
}解决方案:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// 函数式更新 - 始终有最新状态
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(id);
}, []); // 现在安全了
return <div>{count}</div>;
}2. 缺少 Effect 依赖
问题:
function UserSearch({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId); // userId 是一个依赖!
}, []); // Bug: userId 变化时不会重新获取
return <div>{user?.name}</div>;
}解决方案:
function UserSearch({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
let cancelled = false;
async function fetch() {
const data = await fetchUser(userId);
if (!cancelled) setUser(data);
}
fetch();
return () => {
cancelled = true;
};
}, [userId]); // 正确的依赖
return <div>{user?.name}</div>;
}3. 过度记忆化
问题:
function TodoList({ todos }: { todos: Todo[] }) {
// 不必要 - React 已经很快
const memoizedTodos = useMemo(() => todos, [todos]);
// 不必要 - 简单函数
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<ul>
{memoizedTodos.map(todo => (
<li key={todo.id} onClick={handleClick}>
{todo.text}
</li>
))}
</ul>
);
}解决方案:
function TodoList({ todos }: { todos: Todo[] }) {
// 仅记忆化昂贵计算
const completedCount = useMemo(
() => todos.filter(t => t.completed).length,
[todos]
);
// 仅对传递给 memoized 子组件的 props 使用 useCallback
return (
<div>
<p>Completed: {completedCount}</p>
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
</div>
);
}记忆化规则:
- 优化前先测量
- 仅记忆化昂贵计算
- 记忆化传递给 memoized 子组件的回调
- 不要默认记忆化所有内容
---
迁移清单
迁移前:
- [ ] 为当前类组件添加测试
- [ ] 识别所有使用的生命周期方法
- [ ] 记录 props、state 和行为
- [ ] 检查错误边界需求
- [ ] 验证无第三方类继承
迁移中:
- [ ] 转换构造函数/state 到 useState
- [ ] 映射生命周期方法到 useEffect
- [ ] 转换方法到函数或 useCallback
- [ ] 替换 this.setState 为 state setters
- [ ] 更新 ref 使用到 useRef
- [ ] 添加正确的 effect 依赖
- [ ] 在需要的地方添加清理函数
迁移后:
- [ ] 所有测试通过
- [ ] 未添加 eslint-disable 注释
- [ ] 性能等同或更好
- [ ] TypeScript 类型完整
- [ ] 代码审查完成
- [ ] 文档已更新
---
渐进式迁移策略
阶段 1: 新代码
- 使用 hooks 编写所有新组件
- 建立团队模式和约定
阶段 2: 叶子组件
- 首先迁移没有子组件的组件
- 建立信心和肌肉记忆
阶段 3: 容器组件
- 迁移父组件
- 提取自定义 hooks 用于可重用逻辑
阶段 4: 核心基础设施
- 迁移 providers 和 contexts
- 更新路由和状态管理
永不:
- 不要一次性迁移所有内容
- 不要不必要地迁移稳定代码
- 不要为了纯粹性而破坏工作功能
---
本迁移指南提供了实用模式,用于现代化 React 代码库,同时避免常见陷阱并在整个过渡过程中保持代码质量。
性能优化
React.memo
import { memo } from 'react';
// 记忆化组件 - 仅在 props 变化时重新渲染
const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) {
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
});
// 自定义比较函数
const UserCard = memo(
function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);防止重新渲染
// 问题: 每次渲染创建新对象/函数
function Parent() {
// ❌ 每次渲染创建新对象
return <Child style={{ color: 'red' }} onClick={() => doSomething()} />;
}
// 解决方案: 记忆化或提升
const style = { color: 'red' }; // 提升到外部
function Parent() {
const handleClick = useCallback(() => doSomething(), []);
return <Child style={style} onClick={handleClick} />;
}使用 lazy() 代码分割
import { lazy, Suspense } from 'react';
// 分割重型组件
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function App() {
return (
<Suspense fallback={<Loading />}>
{showChart && <HeavyChart data={data} />}
</Suspense>
);
}
// 基于路由的分割 (React Router)
const routes = [
{
path: '/admin',
element: (
<Suspense fallback={<Loading />}>
<AdminPanel />
</Suspense>
),
},
];虚拟化
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: virtualItem.start,
height: virtualItem.size,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}useMemo 用于昂贵计算
function Analytics({ data }: { data: DataPoint[] }) {
// 仅在数据变化时重新计算
const stats = useMemo(() => ({
total: data.reduce((sum, d) => sum + d.value, 0),
average: data.reduce((sum, d) => sum + d.value, 0) / data.length,
max: Math.max(...data.map(d => d.value)),
}), [data]);
return <StatsDisplay stats={stats} />;
}useTransition 用于非紧急更新
import { useTransition } from 'react';
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Item[]>([]);
const [isPending, startTransition] = useTransition();
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value); // 紧急: 立即更新输入
startTransition(() => {
// 非紧急: 可以被中断
setResults(filterItems(e.target.value));
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <Results items={results} />}
</>
);
}快速参考
| 技术 | 何时使用 |
|---|---|
memo() | 防止 props 未变化时的重新渲染 |
useMemo() | 缓存昂贵计算 |
useCallback() | 稳定的函数引用 |
lazy() | 代码分割重型组件 |
useTransition() | 在更新期间保持 UI 响应 |
| 虚拟化 | 大型列表(1000+ 项) |
| 反模式 | 修复方法 |
|---|---|
| 内联对象 | 提升或使用 useMemo |
| 内联函数 | 使用 useCallback |
| 大型打包 | lazy() + Suspense |
| 长列表 | 虚拟化 |
React 19 特性
use() Hook
import { use, Suspense } from 'react';
// 在 render 中读取 promises
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise);
return (
<ul>
{comments.map(c => <li key={c.id}>{c.text}</li>)}
</ul>
);
}
// 父组件创建 promise,子组件读取
function Post({ postId }: { postId: string }) {
const commentsPromise = fetchComments(postId);
return (
<article>
<PostContent id={postId} />
<Suspense fallback={<CommentsSkeleton />}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</article>
);
}
// 条件性读取 context
function Theme({ children }: { children: React.ReactNode }) {
if (someCondition) {
const theme = use(ThemeContext);
return <div className={theme}>{children}</div>;
}
return children;
}useActionState
'use client';
import { useActionState } from 'react';
interface FormState {
error?: string;
success?: boolean;
}
async function submitAction(prevState: FormState, formData: FormData): Promise<FormState> {
'use server';
const email = formData.get('email') as string;
try {
await subscribe(email);
return { success: true };
} catch {
return { error: 'Failed to subscribe' };
}
}
function NewsletterForm() {
const [state, formAction, isPending] = useActionState(submitAction, {});
return (
<form action={formAction}>
<input name="email" type="email" required disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Subscribing...' : 'Subscribe'}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.success && <p className="success">Subscribed!</p>}
</form>
);
}useFormStatus
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
// 必须在 <form> 内部使用
function ContactForm() {
return (
<form action={submitAction}>
<input name="message" />
<SubmitButton />
</form>
);
}useOptimistic
'use client';
import { useOptimistic } from 'react';
function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
);
async function addTodo(formData: FormData) {
const text = formData.get('text') as string;
// 立即更新 UI
addOptimisticTodo({ id: 'temp', text, completed: false });
// 然后持久化
await createTodo(text);
}
return (
<>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<form action={addTodo}>
<input name="text" />
<button>Add</button>
</form>
</>
);
}ref 作为 Prop (不需要 forwardRef)
// React 19: ref 只是一个 prop
function Input({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// 不再需要 forwardRef
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return <Input ref={inputRef} placeholder="Enter text" />;
}快速参考
| Hook | 用途 |
|---|---|
use() | 在 render 中读取 promise/context |
useActionState() | 表单 action 状态 + pending |
useFormStatus() | 表单 pending 状态(子组件) |
useOptimistic() | 乐观 UI 更新 |
| 模式 | 何时使用 |
|---|---|
use(promise) | Suspense 数据获取 |
use(context) | 条件性 context 读取 |
useActionState | 带状态的 Server Actions |
Server Components
Server vs Client Components
// Server Component (App Router 中的默认)
// 可以: 获取数据、访问后端、使用 async/await
// 不能: 使用 hooks、浏览器 API、事件处理器
async function ProductList() {
const products = await db.products.findMany();
return (
<ul>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</ul>
);
}
// Client Component (显式标记)
'use client';
import { useState } from 'react';
function AddToCartButton({ productId }: { productId: string }) {
const [loading, setLoading] = useState(false);
return (
<button onClick={() => addToCart(productId)} disabled={loading}>
Add to Cart
</button>
);
}数据获取模式
// app/products/page.tsx
export default async function ProductsPage() {
// 仅在服务器上运行 - 无客户端打包影响
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 } // 缓存 1 小时
}).then(res => res.json());
return <ProductGrid products={products} />;
}
// 并行数据获取
async function Dashboard() {
const [user, orders, recommendations] = await Promise.all([
getUser(),
getOrders(),
getRecommendations(),
]);
return (
<>
<UserHeader user={user} />
<OrderList orders={orders} />
<Recommendations items={recommendations} />
</>
);
}使用 Suspense 流式传输
import { Suspense } from 'react';
async function SlowComponent() {
const data = await slowFetch(); // 3 秒 API 调用
return <div>{data}</div>;
}
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
<FastComponent />
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
</main>
);
}传递数据 Server → Client
// Server Component
async function ProductPage({ id }: { id: string }) {
const product = await getProduct(id);
// 传递可序列化数据到客户端
return (
<div>
<h1>{product.name}</h1>
{/* Client component 接收序列化的 props */}
<AddToCartButton productId={product.id} price={product.price} />
</div>
);
}Server Actions
// actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.posts.create({ data: { title } });
revalidatePath('/posts');
}
// page.tsx (Server Component)
import { createPost } from './actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
);
}快速参考
| 类型 | 可以使用 | 不能使用 |
|---|---|---|
| Server | async/await、db、fs | useState、onClick |
| Client | hooks、events、浏览器 API | async 组件 |
| 模式 | 使用场景 |
|---|---|
| Server Component | 数据获取、大型依赖 |
| Client Component | 交互、状态 |
'use client' | 标记客户端边界 |
'use server' | Server Action |
| Suspense | 流式传输、加载状态 |
状态管理
本地状态 (useState)
function Counter() {
const [count, setCount] = useState(0);
// 函数式更新用于派生状态
const increment = () => setCount(prev => prev + 1);
return <button onClick={increment}>{count}</button>;
}Context 用于简单全局状态
interface ThemeContext {
theme: 'light' | 'dark';
toggle: () => void;
}
const ThemeContext = createContext<ThemeContext | null>(null);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggle = useCallback(() => {
setTheme(t => t === 'light' ? 'dark' : 'light');
}, []);
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be inside ThemeProvider');
return context;
}Zustand (推荐)
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clear: () => void;
total: () => number;
}
const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(i => i.id !== id)
})),
clear: () => set({ items: [] }),
total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}),
{ name: 'cart-storage' }
)
);
// 组件使用
function Cart() {
const items = useCartStore((state) => state.items);
const total = useCartStore((state) => state.total());
const clear = useCartStore((state) => state.clear);
return (
<div>
{items.map(item => <CartItem key={item.id} item={item} />)}
<p>Total: ${total}</p>
<button onClick={clear}>Clear Cart</button>
</div>
);
}Redux Toolkit
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
incrementBy: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
const store = configureStore({
reducer: { counter: counterSlice.reducer },
});
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
// 类型化 hooks
const useAppSelector = useSelector.withTypes<RootState>();
const useAppDispatch = useDispatch.withTypes<AppDispatch>();
function Counter() {
const count = useAppSelector((state) => state.counter.value);
const dispatch = useAppDispatch();
return (
<button onClick={() => dispatch(counterSlice.actions.increment())}>
{count}
</button>
);
}TanStack Query (服务器状态)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 分钟
});
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
if (isLoading) return <Skeleton />;
if (error) return <Error error={error} />;
return <UserCard user={data} onUpdate={mutation.mutate} />;
}快速参考
| 方案 | 最适合用于 |
|---|---|
| useState | 本地组件状态 |
| Context | 主题、认证、简单全局状态 |
| Zustand | 中等复杂度、最少的样板代码 |
| Redux Toolkit | 复杂状态、中间件、devtools |
| TanStack Query | 服务器状态、缓存 |
React 测试
基础组件测试
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('renders greeting', () => {
render(<Greeting name="World" />);
expect(screen.getByText('Hello, World!')).toBeInTheDocument();
});
test('increments counter on click', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('1')).toBeInTheDocument();
});查询优先级
// 推荐: 可访问性查询(用户如何查找元素)
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText('Email');
screen.getByPlaceholderText('Search...');
screen.getByText('Welcome');
// 备选: Test IDs(当没有可访问名称时)
screen.getByTestId('custom-element');
// 异步查询(等待元素)
await screen.findByText('Loading complete');测试表单
test('submits form with user data', async () => {
const handleSubmit = vi.fn();
const user = userEvent.setup();
render(<ContactForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText('Name'), 'John Doe');
await user.type(screen.getByLabelText('Email'), 'john@example.com');
await user.selectOptions(screen.getByLabelText('Topic'), 'support');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com',
topic: 'support',
});
});使用 Providers 测试
function renderWithProviders(
ui: React.ReactElement,
{ initialState = {}, ...options } = {}
) {
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
{children}
</ThemeProvider>
</QueryClientProvider>
);
}
return render(ui, { wrapper: Wrapper, ...options });
}
test('displays user data', async () => {
renderWithProviders(<UserProfile userId="123" />);
await screen.findByText('John Doe');
});Mock API 调用
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'John' });
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('fetches and displays user', async () => {
render(<UserProfile userId="123" />);
await screen.findByText('John');
});
test('handles error', async () => {
server.use(
http.get('/api/users/:id', () => {
return new HttpResponse(null, { status: 500 });
})
);
render(<UserProfile userId="123" />);
await screen.findByText('Error loading user');
});测试 Hooks
import { renderHook, act } from '@testing-library/react';
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('useDebounce delays value', async () => {
vi.useFakeTimers();
const { result, rerender } = renderHook(
({ value }) => useDebounce(value, 500),
{ initialProps: { value: 'initial' } }
);
rerender({ value: 'updated' });
expect(result.current).toBe('initial');
await act(async () => {
vi.advanceTimersByTime(500);
});
expect(result.current).toBe('updated');
vi.useRealTimers();
});快速参考
| 查询 | 使用时机 |
|---|---|
getByRole | 按钮、链接、标题 |
getByLabelText | 表单输入 |
getByText | 非交互式文本 |
findByX | 异步/加载内容 |
queryByX | 断言不存在 |
| 模式 | 使用场景 |
|---|---|
userEvent.setup() | 用户交互 |
renderHook() | 测试自定义 hooks |
msw | Mock API 调用 |
| 自定义 render | 包装 providers |