| title | React 组件开发规范 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| type | context_aware | ||||||||||
| priority | high | ||||||||||
| description | React 组件的开发规范和最佳实践 | ||||||||||
| enabled | true | ||||||||||
| tags |
|
||||||||||
| triggers |
|
✅ 推荐的组件结构:
import React, { useState, useEffect, useCallback } from 'react';
import './MyComponent.css';
interface MyComponentProps {
title: string;
onSave?: (data: Data) => void;
className?: string;
}
export const MyComponent: React.FC<MyComponentProps> = ({
title,
onSave,
className
}) => {
// 🎯 1. State 声明
const [data, setData] = useState<Data | null>(null);
const [loading, setLoading] = useState(false);
// 🎯 2. Refs
const inputRef = useRef<HTMLInputElement>(null);
// 🎯 3. 自定义 Hooks
const { user } = useAuth();
// 🎯 4. useEffect(副作用)
useEffect(() => {
loadData();
return () => cleanup();
}, []);
// 🎯 5. 事件处理函数
const handleSave = useCallback(() => {
if (data) {
onSave?.(data);
}
}, [data, onSave]);
// 🎯 6. 辅助函数
const loadData = async () => {
setLoading(true);
try {
const result = await fetchData();
setData(result);
} finally {
setLoading(false);
}
};
// 🎯 7. 条件渲染
if (loading) {
return <LoadingSpinner />;
}
if (!data) {
return <EmptyState />;
}
// 🎯 8. 主渲染
return (
<div className={`my-component ${className || ''}`}>
<h2>{title}</h2>
<DataDisplay data={data} />
<button onClick={handleSave}>Save</button>
</div>
);
};- Props 接口命名:
{ComponentName}Props - 可选 props 使用
? - 回调函数以
on开头
interface UserCardProps {
user: User;
editable?: boolean;
onEdit?: (user: User) => void;
onDelete?: (userId: string) => void;
className?: string;
style?: React.CSSProperties;
}使用解构赋值设置默认值:
export const UserCard: React.FC<UserCardProps> = ({
user,
editable = false,
onEdit,
className = '',
}) => {
// ...
};正确处理 children:
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
export const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
};- 使用描述性的状态名称
- 复杂状态使用
useReducer
// ✅ 好的命名
const [isLoading, setIsLoading] = useState(false);
const [userData, setUserData] = useState<User | null>(null);
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
// ❌ 不好的命名
const [flag, setFlag] = useState(false);
const [data, setData] = useState(null);- 明确依赖项数组
- 返回清理函数(如果需要)
- 拆分不相关的副作用
✅ 推荐:
// 独立的副作用
useEffect(() => {
document.title = `User: ${user.name}`;
}, [user.name]);
useEffect(() => {
const timer = setInterval(() => {
checkStatus();
}, 5000);
return () => clearInterval(timer);
}, []);❌ 不推荐:
// 混合多个不相关的副作用
useEffect(() => {
document.title = `User: ${user.name}`;
const timer = setInterval(() => checkStatus(), 5000);
return () => clearInterval(timer);
}, [user.name]);仅在必要时使用:
// ✅ 好的用法 - 传递给子组件的回调
const handleClick = useCallback(() => {
processData(data);
}, [data]);
<ExpensiveChild onClick={handleClick} />
// ✅ 好的用法 - 昂贵的计算
const sortedItems = useMemo(
() => items.sort((a, b) => heavyComparison(a, b)),
[items]
);
// ❌ 不必要的优化
const simpleValue = useMemo(() => x + y, [x, y]);提取可复用逻辑到自定义 Hooks:
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
// 使用
const [user, setUser] = useLocalStorage<User>('user', defaultUser);// ✅ 简单条件
{isLoggedIn && <UserProfile />}
{error && <ErrorMessage error={error} />}
// ⚠️ 注意:0 和空字符串会被渲染
{count && <span>{count} items</span>} // 当 count=0 时会显示 "0"
// ✅ 正确做法
{count > 0 && <span>{count} items</span>}// ✅ 简单二选一
{isLoading ? <Spinner /> : <Content />}
// ❌ 嵌套过深
{isLoading ? (
<Spinner />
) : hasError ? (
<Error />
) : hasData ? (
<Content data={data} />
) : (
<Empty />
)}
// ✅ 提前返回或使用辅助函数
const renderContent = () => {
if (isLoading) return <Spinner />;
if (hasError) return <Error />;
if (!hasData) return <Empty />;
return <Content data={data} />;
};
return <div>{renderContent()}</div>;// ✅ 使用稳定的唯一 ID
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
// ❌ 使用索引(除非列表静态且不会重排序)
{users.map((user, index) => (
<UserCard key={index} user={user} />
))}// ✅ 使用 React.memo 避免不必要的重渲染
export const UserCard = React.memo<UserCardProps>(({ user, onEdit }) => {
return (
<div className="user-card">
<h3>{user.name}</h3>
<button onClick={() => onEdit(user)}>Edit</button>
</div>
);
});
// 自定义比较函数
export const UserCard = React.memo<UserCardProps>(
({ user, onEdit }) => { /* ... */ },
(prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
}
);优先使用 CSS Modules:
import styles from './MyComponent.module.css';
export const MyComponent: React.FC = () => {
return (
<div className={styles.container}>
<h2 className={styles.title}>Title</h2>
</div>
);
};使用 classnames 库或模板字符串:
import classNames from 'classnames';
const buttonClass = classNames(
styles.button,
{
[styles.active]: isActive,
[styles.disabled]: isDisabled,
},
className
);
// 或使用模板字符串
const buttonClass = `${styles.button} ${isActive ? styles.active : ''} ${className || ''}`;export const UserForm: React.FC<UserFormProps> = ({ onSubmit }) => {
const [formData, setFormData] = useState({
name: '',
email: '',
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<button type="submit">Submit</button>
</form>
);
};interface FormErrors {
[key: string]: string;
}
export const UserForm: React.FC = () => {
const [formData, setFormData] = useState({ name: '', email: '' });
const [errors, setErrors] = useState<FormErrors>({});
const validate = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.email.includes('@')) {
newErrors.email = 'Invalid email format';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validate()) {
// Submit form
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input name="name" value={formData.name} onChange={handleChange} />
{errors.name && <span className="error">{errors.name}</span>}
</div>
{/* ... */}
</form>
);
};创建错误边界组件:
interface ErrorBoundaryProps {
children: React.ReactNode;
fallback?: React.ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
</div>
);
}
return this.props.children;
}
}
// 使用
<ErrorBoundary fallback={<ErrorFallback />}>
<MyComponent />
</ErrorBoundary>import { render, screen, fireEvent } from '@testing-library/react';
import { UserCard } from './UserCard';
describe('UserCard', () => {
const mockUser = {
id: '1',
name: 'John Doe',
email: 'john@example.com',
};
it('renders user information', () => {
render(<UserCard user={mockUser} />);
expect(screen.getByText(mockUser.name)).toBeInTheDocument();
expect(screen.getByText(mockUser.email)).toBeInTheDocument();
});
it('calls onEdit when edit button is clicked', () => {
const handleEdit = jest.fn();
render(<UserCard user={mockUser} onEdit={handleEdit} />);
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
expect(handleEdit).toHaveBeenCalledWith(mockUser);
});
});// ✅ 好的做法
<button onClick={handleClick}>Click me</button>
<nav><ul><li><a href="/home">Home</a></li></ul></nav>
// ❌ 不好的做法
<div onClick={handleClick}>Click me</div>
<div className="nav"><div className="menu">...</div></div><button
onClick={handleToggle}
aria-label="Toggle menu"
aria-expanded={isOpen}
aria-controls="menu-items"
>
Menu
</button>
<div id="menu-items" role="menu" hidden={!isOpen}>
{/* Menu items */}
</div>遵循这些 React 组件开发规范可以提高代码质量、可维护性和团队协作效率。