Appearance
ref和Context改进实战
学习目标
通过本章学习,你将掌握:
- ref和Context改进的综合应用
- 实际项目案例
- 组合使用技巧
- 性能优化策略
- 代码重构实践
- 迁移最佳实践
- 常见问题解决
- 企业级应用场景
第一部分:表单组件库
1.1 简化的Input组件
jsx
// ✅ 使用ref作为prop的Input组件
function Input({ ref, label, error, helperText, ...props }) {
return (
<div className="input-field">
{label && <label>{label}</label>}
<input
ref={ref}
className={error ? 'input-error' : 'input-normal'}
{...props}
/>
{error && <span className="error-text">{error}</span>}
{helperText && <span className="helper-text">{helperText}</span>}
</div>
);
}
// 使用
function LoginForm() {
const emailRef = useRef(null);
const passwordRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const email = emailRef.current.value;
const password = passwordRef.current.value;
// 验证
if (!email) {
emailRef.current.focus();
return;
}
if (!password) {
passwordRef.current.focus();
return;
}
// 提交
login({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<Input
ref={emailRef}
label="邮箱"
type="email"
placeholder="输入邮箱"
helperText="我们不会分享您的邮箱"
/>
<Input
ref={passwordRef}
label="密码"
type="password"
placeholder="输入密码"
/>
<button type="submit">登录</button>
</form>
);
}1.2 表单Context管理
jsx
// ✅ 简化的表单Context
import { createContext, useContext, useState } from 'react';
const FormContext = createContext(null);
export function Form({ onSubmit, children, initialValues = {} }) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const setValue = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
// 清除错误
if (errors[name]) {
setErrors(prev => {
const next = { ...prev };
delete next[name];
return next;
});
}
};
const setError = (name, error) => {
setErrors(prev => ({ ...prev, [name]: error }));
};
const setFieldTouched = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
};
const handleSubmit = (e) => {
e.preventDefault();
// 标记所有字段为已触摸
const allFields = Object.keys(values);
setTouched(
allFields.reduce((acc, field) => ({ ...acc, [field]: true }), {})
);
// 验证
const hasErrors = Object.keys(errors).length > 0;
if (!hasErrors) {
onSubmit(values);
}
};
const contextValue = {
values,
errors,
touched,
setValue,
setError,
setFieldTouched
};
return (
<FormContext value={contextValue}>
<form onSubmit={handleSubmit}>
{children}
</form>
</FormContext>
);
}
export function useFormField(name, validation) {
const context = useContext(FormContext);
if (!context) {
throw new Error('useFormField must be used within Form');
}
const { values, errors, touched, setValue, setError, setFieldTouched } = context;
const value = values[name] || '';
const error = errors[name];
const isTouched = touched[name];
const handleChange = (e) => {
const newValue = e.target.value;
setValue(name, newValue);
// 验证
if (validation) {
const validationError = validation(newValue);
if (validationError) {
setError(name, validationError);
}
}
};
const handleBlur = () => {
setFieldTouched(name);
};
return {
value,
error: isTouched ? error : undefined,
onChange: handleChange,
onBlur: handleBlur
};
}
// 使用
function RegistrationForm() {
const handleSubmit = (values) => {
console.log('Form submitted:', values);
api.register(values);
};
return (
<Form onSubmit={handleSubmit}>
<EmailField />
<PasswordField />
<ConfirmPasswordField />
<button type="submit">注册</button>
</Form>
);
}
function EmailField() {
const field = useFormField('email', (value) => {
if (!value) return '邮箱不能为空';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return '请输入有效的邮箱地址';
}
});
return (
<Input
label="邮箱"
type="email"
error={field.error}
{...field}
/>
);
}
function PasswordField() {
const field = useFormField('password', (value) => {
if (!value) return '密码不能为空';
if (value.length < 8) return '密码至少8个字符';
});
return (
<Input
label="密码"
type="password"
error={field.error}
{...field}
/>
);
}第二部分:主题系统
2.1 完整的主题管理
jsx
// ✅ 使用简化Context的主题系统
import { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
// 从localStorage读取
const saved = localStorage.getItem('theme');
return saved || 'light';
});
const [customColors, setCustomColors] = useState({
primary: '#3b82f6',
secondary: '#8b5cf6',
success: '#10b981',
error: '#ef4444'
});
useEffect(() => {
// 保存到localStorage
localStorage.setItem('theme', theme);
// 更新CSS变量
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
useEffect(() => {
// 更新自定义颜色
Object.entries(customColors).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--color-${key}`, value);
});
}, [customColors]);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const updateColor = (key, value) => {
setCustomColors(prev => ({ ...prev, [key]: value }));
};
const value = {
theme,
customColors,
toggleTheme,
updateColor,
isDark: theme === 'dark'
};
return (
<ThemeContext value={value}>
{children}
</ThemeContext>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// 使用
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
function ColorPicker() {
const { customColors, updateColor } = useTheme();
return (
<div className="color-picker">
<h3>自定义颜色</h3>
{Object.entries(customColors).map(([key, value]) => (
<div key={key} className="color-input">
<label>{key}</label>
<input
type="color"
value={value}
onChange={(e) => updateColor(key, e.target.value)}
/>
</div>
))}
</div>
);
}第三部分:Modal系统
3.2 Modal管理器
jsx
// ✅ 使用ref callback清理的Modal
function Modal({ ref, isOpen, onClose, children }) {
const modalRef = (element) => {
if (!element || !isOpen) return;
// 聚焦Modal
element.focus();
// Esc键关闭
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
// 点击外部关闭
const handleClickOutside = (e) => {
if (e.target === element) {
onClose();
}
};
document.addEventListener('keydown', handleKeyDown);
element.addEventListener('click', handleClickOutside);
// 锁定滚动
document.body.style.overflow = 'hidden';
return () => {
document.removeEventListener('keydown', handleKeyDown);
element.removeEventListener('click', handleClickOutside);
document.body.style.overflow = '';
};
};
if (!isOpen) return null;
return (
<div ref={modalRef} className="modal-overlay" tabIndex={-1}>
<div className="modal-content">
{children}
</div>
</div>
);
}
// Modal Context
const ModalContext = createContext(null);
export function ModalProvider({ children }) {
const [modals, setModals] = useState([]);
const openModal = (id, content, options = {}) => {
setModals(prev => [...prev, { id, content, options }]);
};
const closeModal = (id) => {
setModals(prev => prev.filter(modal => modal.id !== id));
};
const closeAll = () => {
setModals([]);
};
const value = {
openModal,
closeModal,
closeAll
};
return (
<ModalContext value={value}>
{children}
{modals.map(modal => (
<Modal
key={modal.id}
isOpen={true}
onClose={() => closeModal(modal.id)}
>
{modal.content}
</Modal>
))}
</ModalContext>
);
}
export function useModal() {
const context = useContext(ModalContext);
if (!context) {
throw new Error('useModal must be used within ModalProvider');
}
return context;
}
// 使用
function App() {
return (
<ModalProvider>
<MainApp />
</ModalProvider>
);
}
function MainApp() {
const { openModal, closeModal } = useModal();
const handleOpenConfirm = () => {
const modalId = 'confirm-delete';
openModal(
modalId,
<div>
<h2>确认删除</h2>
<p>确定要删除这个项目吗?</p>
<button onClick={() => {
// 执行删除
deleteItem();
closeModal(modalId);
}}>
确认
</button>
<button onClick={() => closeModal(modalId)}>
取消
</button>
</div>
);
};
return (
<div>
<button onClick={handleOpenConfirm}>删除项目</button>
</div>
);
}第四部分:可访问性增强
4.1 焦点管理
jsx
// ✅ 使用ref管理焦点
function FocusTrap({ children }) {
const containerRef = (element) => {
if (!element) return;
// 查找所有可聚焦元素
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleKeyDown = (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
// Tab
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
element.addEventListener('keydown', handleKeyDown);
// 聚焦第一个元素
firstElement?.focus();
return () => {
element.removeEventListener('keydown', handleKeyDown);
};
};
return (
<div ref={containerRef}>
{children}
</div>
);
}
// 使用在Dialog中
function Dialog({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div className="dialog-overlay">
<FocusTrap>
<div className="dialog" role="dialog" aria-modal="true">
{children}
<button onClick={onClose}>关闭</button>
</div>
</FocusTrap>
</div>
);
}4.2 键盘导航
jsx
// ✅ 可键盘导航的列表
function NavigableList({ items }) {
const [activeIndex, setActiveIndex] = useState(0);
const itemRefs = useRef([]);
const listRef = (element) => {
if (!element) return;
const handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(prev =>
Math.min(prev + 1, items.length - 1)
);
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(prev => Math.max(prev - 1, 0));
break;
case 'Home':
e.preventDefault();
setActiveIndex(0);
break;
case 'End':
e.preventDefault();
setActiveIndex(items.length - 1);
break;
case 'Enter':
if (itemRefs.current[activeIndex]) {
itemRefs.current[activeIndex].click();
}
break;
}
};
element.addEventListener('keydown', handleKeyDown);
return () => {
element.removeEventListener('keydown', handleKeyDown);
};
};
useEffect(() => {
// 滚动到激活项
itemRefs.current[activeIndex]?.scrollIntoView({
block: 'nearest'
});
}, [activeIndex]);
return (
<ul ref={listRef} role="listbox" tabIndex={0}>
{items.map((item, index) => (
<li
key={item.id}
ref={el => itemRefs.current[index] = el}
role="option"
aria-selected={index === activeIndex}
className={index === activeIndex ? 'active' : ''}
>
{item.label}
</li>
))}
</ul>
);
}第五部分:性能监控
5.1 渲染性能追踪
jsx
// ✅ 使用ref callback追踪渲染
function PerformanceMonitor({ children, componentName }) {
const renderCountRef = useRef(0);
const lastRenderTime = useRef(Date.now());
const ref = (element) => {
if (!element) return;
renderCountRef.current += 1;
const now = Date.now();
const timeSinceLastRender = now - lastRenderTime.current;
lastRenderTime.current = now;
console.log(`[${componentName}] 渲染 #${renderCountRef.current}`, {
timeSinceLastRender: `${timeSinceLastRender}ms`
});
};
return <div ref={ref}>{children}</div>;
}
// 使用
function App() {
return (
<PerformanceMonitor componentName="App">
<MainContent />
</PerformanceMonitor>
);
}注意事项
1. 正确处理清理
jsx
// ✅ 确保所有资源都被清理
const ref = (element) => {
if (!element) return;
const subscription = subscribe();
const timer = setInterval(() => {}, 1000);
element.addEventListener('click', handler);
return () => {
subscription.unsubscribe();
clearInterval(timer);
element.removeEventListener('click', handler);
};
};2. Context默认值
jsx
// ✅ 提供有意义的默认值
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {
console.warn('toggleTheme called outside ThemeProvider');
}
});3. 类型安全
tsx
// ✅ TypeScript类型
interface ThemeContextValue {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue>({
theme: 'light',
toggleTheme: () => {}
});第六部分:企业级应用案例
6.1 数据表格系统
jsx
// ✅ 带排序、过滤、分页的数据表格
const TableContext = createContext(null);
export function DataTable({ data, columns, children }) {
const [sortBy, setSortBy] = useState(null);
const [sortOrder, setSortOrder] = useState('asc');
const [filters, setFilters] = useState({});
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
// 处理数据
const processedData = useMemo(() => {
let result = [...data];
// 过滤
Object.entries(filters).forEach(([key, value]) => {
if (value) {
result = result.filter(item =>
String(item[key]).toLowerCase().includes(value.toLowerCase())
);
}
});
// 排序
if (sortBy) {
result.sort((a, b) => {
const aVal = a[sortBy];
const bVal = b[sortBy];
const order = sortOrder === 'asc' ? 1 : -1;
return aVal > bVal ? order : -order;
});
}
return result;
}, [data, filters, sortBy, sortOrder]);
// 分页数据
const paginatedData = useMemo(() => {
const start = (page - 1) * pageSize;
return processedData.slice(start, start + pageSize);
}, [processedData, page, pageSize]);
const value = {
data: paginatedData,
allData: processedData,
columns,
sortBy,
sortOrder,
filters,
page,
pageSize,
totalPages: Math.ceil(processedData.length / pageSize),
setSortBy,
setSortOrder,
setFilters,
setPage,
setPageSize,
handleSort: (columnKey) => {
if (sortBy === columnKey) {
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
setSortBy(columnKey);
setSortOrder('asc');
}
}
};
return (
<TableContext value={value}>
{children}
</TableContext>
);
}
export function useTable() {
const context = useContext(TableContext);
if (!context) {
throw new Error('useTable must be used within DataTable');
}
return context;
}
// 表格组件
function TableHeader() {
const { columns, sortBy, sortOrder, handleSort } = useTable();
return (
<thead>
<tr>
{columns.map(column => (
<th key={column.key} onClick={() => handleSort(column.key)}>
{column.label}
{sortBy === column.key && (
<span>{sortOrder === 'asc' ? ' ↑' : ' ↓'}</span>
)}
</th>
))}
</tr>
</thead>
);
}
function TableBody() {
const { data, columns } = useTable();
return (
<tbody>
{data.map((row, index) => (
<tr key={index}>
{columns.map(column => (
<td key={column.key}>
{column.render ? column.render(row[column.key], row) : row[column.key]}
</td>
))}
</tr>
))}
</tbody>
);
}
function TablePagination() {
const { page, totalPages, setPage, pageSize, setPageSize } = useTable();
return (
<div className="pagination">
<button
onClick={() => setPage(1)}
disabled={page === 1}
>
首页
</button>
<button
onClick={() => setPage(prev => Math.max(1, prev - 1))}
disabled={page === 1}
>
上一页
</button>
<span>第 {page} / {totalPages} 页</span>
<button
onClick={() => setPage(prev => Math.min(totalPages, prev + 1))}
disabled={page === totalPages}
>
下一页
</button>
<button
onClick={() => setPage(totalPages)}
disabled={page === totalPages}
>
末页
</button>
<select value={pageSize} onChange={(e) => setPageSize(Number(e.target.value))}>
<option value={10}>10 条/页</option>
<option value={25}>25 条/页</option>
<option value={50}>50 条/页</option>
</select>
</div>
);
}
// 使用示例
function UserManagement() {
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'Admin' },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'User' },
// ... more users
];
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'name', label: '姓名' },
{ key: 'email', label: '邮箱' },
{
key: 'role',
label: '角色',
render: (value) => (
<span className={`role-badge ${value.toLowerCase()}`}>{value}</span>
)
}
];
return (
<DataTable data={users} columns={columns}>
<div className="table-container">
<table>
<TableHeader />
<TableBody />
</table>
<TablePagination />
</div>
</DataTable>
);
}6.2 通知系统
jsx
// ✅ 全局通知管理
const NotificationContext = createContext(null);
export function NotificationProvider({ children }) {
const [notifications, setNotifications] = useState([]);
const addNotification = useCallback((notification) => {
const id = Math.random().toString(36).substr(2, 9);
const newNotification = {
id,
type: 'info',
duration: 3000,
...notification
};
setNotifications(prev => [...prev, newNotification]);
// 自动移除
if (newNotification.duration) {
setTimeout(() => {
removeNotification(id);
}, newNotification.duration);
}
return id;
}, []);
const removeNotification = useCallback((id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
}, []);
const success = useCallback((message, options) => {
return addNotification({ type: 'success', message, ...options });
}, [addNotification]);
const error = useCallback((message, options) => {
return addNotification({ type: 'error', message, ...options });
}, [addNotification]);
const warning = useCallback((message, options) => {
return addNotification({ type: 'warning', message, ...options });
}, [addNotification]);
const info = useCallback((message, options) => {
return addNotification({ type: 'info', message, ...options });
}, [addNotification]);
const value = {
notifications,
addNotification,
removeNotification,
success,
error,
warning,
info
};
return (
<NotificationContext value={value}>
{children}
<NotificationContainer />
</NotificationContext>
);
}
export function useNotification() {
const context = useContext(NotificationContext);
if (!context) {
throw new Error('useNotification must be used within NotificationProvider');
}
return context;
}
// 通知容器
function NotificationContainer() {
const { notifications } = useNotification();
return (
<div className="notification-container">
{notifications.map(notification => (
<Notification key={notification.id} {...notification} />
))}
</div>
);
}
// 单个通知
function Notification({ id, type, message, title }) {
const { removeNotification } = useNotification();
const [isExiting, setIsExiting] = useState(false);
const notificationRef = (element) => {
if (!element) return;
// 入场动画
element.classList.add('notification-enter');
setTimeout(() => {
element.classList.remove('notification-enter');
element.classList.add('notification-enter-active');
}, 10);
return () => {
// 退场清理
element.classList.remove('notification-enter-active');
};
};
const handleClose = () => {
setIsExiting(true);
setTimeout(() => {
removeNotification(id);
}, 300);
};
return (
<div
ref={notificationRef}
className={`notification notification-${type} ${isExiting ? 'notification-exit' : ''}`}
>
{title && <div className="notification-title">{title}</div>}
<div className="notification-message">{message}</div>
<button onClick={handleClose} className="notification-close">×</button>
</div>
);
}
// 使用示例
function UserActions() {
const { success, error } = useNotification();
const handleSave = async () => {
try {
await api.saveUser(userData);
success('用户保存成功!');
} catch (err) {
error('保存失败:' + err.message);
}
};
return <button onClick={handleSave}>保存用户</button>;
}6.3 多步骤向导
jsx
// ✅ 向导流程管理
const WizardContext = createContext(null);
export function Wizard({ children, onComplete }) {
const [currentStep, setCurrentStep] = useState(0);
const [stepData, setStepData] = useState({});
const [completedSteps, setCompletedSteps] = useState(new Set());
const steps = React.Children.toArray(children).filter(
child => child.type === WizardStep
);
const totalSteps = steps.length;
const isFirstStep = currentStep === 0;
const isLastStep = currentStep === totalSteps - 1;
const goToStep = useCallback((step) => {
if (step >= 0 && step < totalSteps) {
setCurrentStep(step);
}
}, [totalSteps]);
const nextStep = useCallback(() => {
if (!isLastStep) {
setCompletedSteps(prev => new Set(prev).add(currentStep));
setCurrentStep(prev => prev + 1);
}
}, [isLastStep, currentStep]);
const prevStep = useCallback(() => {
if (!isFirstStep) {
setCurrentStep(prev => prev - 1);
}
}, [isFirstStep]);
const updateStepData = useCallback((data) => {
setStepData(prev => ({ ...prev, ...data }));
}, []);
const handleComplete = useCallback(() => {
setCompletedSteps(prev => new Set(prev).add(currentStep));
onComplete?.(stepData);
}, [currentStep, stepData, onComplete]);
const value = {
currentStep,
totalSteps,
stepData,
completedSteps,
isFirstStep,
isLastStep,
goToStep,
nextStep,
prevStep,
updateStepData,
handleComplete
};
return (
<WizardContext value={value}>
<div className="wizard">
<WizardProgress />
<div className="wizard-content">
{steps[currentStep]}
</div>
<WizardControls />
</div>
</WizardContext>
);
}
export function WizardStep({ title, children }) {
return <div className="wizard-step">{children}</div>;
}
export function useWizard() {
const context = useContext(WizardContext);
if (!context) {
throw new Error('useWizard must be used within Wizard');
}
return context;
}
// 进度指示器
function WizardProgress() {
const { currentStep, totalSteps, completedSteps, goToStep } = useWizard();
return (
<div className="wizard-progress">
{Array.from({ length: totalSteps }, (_, index) => (
<div
key={index}
className={`progress-step ${
index === currentStep ? 'active' : ''
} ${
completedSteps.has(index) ? 'completed' : ''
}`}
onClick={() => completedSteps.has(index) && goToStep(index)}
>
<div className="step-number">{index + 1}</div>
</div>
))}
</div>
);
}
// 控制按钮
function WizardControls() {
const { isFirstStep, isLastStep, prevStep, nextStep, handleComplete } = useWizard();
return (
<div className="wizard-controls">
<button onClick={prevStep} disabled={isFirstStep}>
上一步
</button>
{!isLastStep ? (
<button onClick={nextStep}>下一步</button>
) : (
<button onClick={handleComplete} className="primary">完成</button>
)}
</div>
);
}
// 使用示例
function RegistrationWizard() {
const handleComplete = (data) => {
console.log('注册完成:', data);
api.register(data);
};
return (
<Wizard onComplete={handleComplete}>
<WizardStep title="个人信息">
<PersonalInfoForm />
</WizardStep>
<WizardStep title="账户设置">
<AccountSettingsForm />
</WizardStep>
<WizardStep title="确认">
<ConfirmationStep />
</WizardStep>
</Wizard>
);
}
function PersonalInfoForm() {
const { updateStepData, stepData } = useWizard();
return (
<div>
<h2>个人信息</h2>
<Input
label="姓名"
value={stepData.name || ''}
onChange={(e) => updateStepData({ name: e.target.value })}
/>
<Input
label="邮箱"
type="email"
value={stepData.email || ''}
onChange={(e) => updateStepData({ email: e.target.value })}
/>
</div>
);
}6.4 拖拽排序系统
jsx
// ✅ 拖拽Context管理
const DragDropContext = createContext(null);
export function DragDropProvider({ children }) {
const [draggedItem, setDraggedItem] = useState(null);
const [dragOverItem, setDragOverItem] = useState(null);
const value = {
draggedItem,
dragOverItem,
setDraggedItem,
setDragOverItem
};
return (
<DragDropContext value={value}>
{children}
</DragDropContext>
);
}
export function useDragDrop() {
const context = useContext(DragDropContext);
if (!context) {
throw new Error('useDragDrop must be used within DragDropProvider');
}
return context;
}
// 可拖拽项
function DraggableItem({ id, index, children, onReorder }) {
const { draggedItem, dragOverItem, setDraggedItem, setDragOverItem } = useDragDrop();
const itemRef = (element) => {
if (!element) return;
const handleDragStart = (e) => {
setDraggedItem({ id, index });
e.dataTransfer.effectAllowed = 'move';
element.classList.add('dragging');
};
const handleDragEnd = () => {
setDraggedItem(null);
setDragOverItem(null);
element.classList.remove('dragging');
};
const handleDragOver = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (draggedItem && draggedItem.index !== index) {
setDragOverItem({ id, index });
}
};
const handleDragLeave = () => {
element.classList.remove('drag-over');
};
const handleDrop = (e) => {
e.preventDefault();
element.classList.remove('drag-over');
if (draggedItem && draggedItem.index !== index) {
onReorder(draggedItem.index, index);
}
};
element.addEventListener('dragstart', handleDragStart);
element.addEventListener('dragend', handleDragEnd);
element.addEventListener('dragover', handleDragOver);
element.addEventListener('dragleave', handleDragLeave);
element.addEventListener('drop', handleDrop);
return () => {
element.removeEventListener('dragstart', handleDragStart);
element.removeEventListener('dragend', handleDragEnd);
element.removeEventListener('dragover', handleDragOver);
element.removeEventListener('dragleave', handleDragLeave);
element.removeEventListener('drop', handleDrop);
};
};
return (
<div
ref={itemRef}
draggable
className={`draggable-item ${
dragOverItem?.index === index ? 'drag-over' : ''
}`}
>
{children}
</div>
);
}
// 使用示例
function SortableList() {
const [items, setItems] = useState([
{ id: 1, text: '项目 1' },
{ id: 2, text: '项目 2' },
{ id: 3, text: '项目 3' },
{ id: 4, text: '项目 4' }
]);
const handleReorder = (fromIndex, toIndex) => {
const newItems = [...items];
const [removed] = newItems.splice(fromIndex, 1);
newItems.splice(toIndex, 0, removed);
setItems(newItems);
};
return (
<DragDropProvider>
<div className="sortable-list">
{items.map((item, index) => (
<DraggableItem
key={item.id}
id={item.id}
index={index}
onReorder={handleReorder}
>
<span className="drag-handle">⋮⋮</span>
{item.text}
</DraggableItem>
))}
</div>
</DragDropProvider>
);
}常见问题
Q1: 如何在现有项目中逐步采用这些改进?
A: 采用渐进式迁移策略,从新功能开始:
步骤1:评估现状
jsx
// 识别需要改进的组件
// 1. 使用forwardRef的组件 -> 迁移到ref as prop
// 2. 使用Context.Provider的组件 -> 迁移到简化语法
// 3. 有复杂清理逻辑的组件 -> 使用ref callback步骤2:制定计划
1. 新功能:直接使用新特性
2. 活跃维护的模块:逐步重构
3. 稳定模块:保持现状,等待重大重构时再迁移
4. 核心库组件:优先迁移,影响面大步骤3:实施迁移
jsx
// 示例:迁移forwardRef组件
// 旧代码
const OldInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
// 新代码(React 19)
function NewInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// 两种方式可以并存,逐步替换步骤4:测试验证
jsx
// 确保迁移后功能正常
test('Input component works after migration', () => {
const ref = React.createRef();
render(<NewInput ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLInputElement);
ref.current.focus();
expect(document.activeElement).toBe(ref.current);
});步骤5:文档更新
- 更新组件文档
- 更新使用示例
- 添加迁移指南
- 培训团队成员
Q2: 这些改进对性能有影响吗?
A: 没有负面影响,某些情况下还能提升性能:
性能对比:
jsx
// ref callback清理 vs useEffect清理
// 旧方式:useEffect
function OldComponent() {
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new ResizeObserver(() => {});
observer.observe(element);
return () => {
observer.disconnect();
};
}, []); // 需要依赖数组管理
return <div ref={ref}>Content</div>;
}
// 新方式:ref callback
function NewComponent() {
const ref = (element) => {
if (!element) return;
const observer = new ResizeObserver(() => {});
observer.observe(element);
return () => observer.disconnect();
};
return <div ref={ref}>Content</div>;
// 更少的代码,更早的清理时机
}性能优势:
- ref callback清理:清理时机更精确,避免不必要的延迟
- Context简化语法:只是语法糖,运行时性能完全相同
- ref as prop:减少了forwardRef的间接层,理论上略快
Q3: 如何测试使用了这些特性的组件?
A: 使用React Testing Library,与普通组件测试相同:
jsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// 测试ref as prop
test('Input ref works correctly', () => {
const ref = React.createRef();
render(<Input ref={ref} placeholder="Test" />);
expect(ref.current).toBeInstanceOf(HTMLInputElement);
expect(ref.current.placeholder).toBe('Test');
// 测试ref方法
ref.current.focus();
expect(document.activeElement).toBe(ref.current);
});
// 测试Context简化语法
test('Theme context works', () => {
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>
);
const button = screen.getByText(/切换主题/i);
fireEvent.click(button);
// 验证主题切换
expect(screen.getByTestId('theme-indicator')).toHaveTextContent('dark');
});
// 测试ref callback清理
test('Cleanup is called on unmount', () => {
const cleanup = jest.fn();
function TestComponent() {
const ref = (element) => {
if (!element) return;
return cleanup;
};
return <div ref={ref}>Test</div>;
}
const { unmount } = render(<TestComponent />);
expect(cleanup).not.toHaveBeenCalled();
unmount();
expect(cleanup).toHaveBeenCalledTimes(1);
});
// 测试复杂交互
test('Modal opens and closes correctly', async () => {
const user = userEvent.setup();
render(
<ModalProvider>
<ModalTrigger />
</ModalProvider>
);
// 打开Modal
await user.click(screen.getByText('打开Modal'));
expect(screen.getByRole('dialog')).toBeInTheDocument();
// 按Esc关闭
await user.keyboard('{Escape}');
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});Q4: 如何处理TypeScript类型?
A: React 19的类型定义已经包含这些改进:
tsx
// ref as prop的类型
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
ref?: React.Ref<HTMLInputElement>; // ref自动包含在props中
label?: string;
error?: string;
}
function Input({ ref, label, error, ...props }: InputProps) {
return (
<div>
{label && <label>{label}</label>}
<input ref={ref} {...props} />
{error && <span>{error}</span>}
</div>
);
}
// Context的类型
interface ThemeContextValue {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue>({
theme: 'light',
toggleTheme: () => {}
});
// 使用时自动推断类型
function Component() {
const { theme, toggleTheme } = useContext(ThemeContext);
// theme的类型是 'light' | 'dark'
// toggleTheme的类型是 () => void
}
// ref callback的类型
type RefCallback<T> = (element: T | null) => (() => void) | void;
const ref: RefCallback<HTMLDivElement> = (element) => {
if (!element) return;
const observer = new ResizeObserver(() => {});
observer.observe(element);
return () => {
observer.disconnect();
};
};Q5: 这些特性与React 18兼容吗?
A: 部分兼容:
| 特性 | React 18 | React 19 |
|---|---|---|
| ref as prop | ❌ 需要forwardRef | ✅ 直接支持 |
| Context简化 | ❌ 必须用.Provider | ✅ 可省略.Provider |
| ref callback清理 | ❌ 不支持返回清理函数 | ✅ 支持 |
兼容性处理:
jsx
// 方法1:条件编译
import { version } from 'react';
const isReact19 = parseInt(version) >= 19;
function Input({ ref, ...props }) {
if (isReact19) {
return <input ref={ref} {...props} />;
} else {
// React 18降级方案
return <InputWithForwardRef ref={ref} {...props} />;
}
}
// 方法2:统一使用React 19语法,通过polyfill支持React 18
// (需要额外的构建配置)
// 方法3:保持双版本支持
export const InputV18 = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
export const InputV19 = ({ ref, ...props }) => {
return <input ref={ref} {...props} />;
};
export const Input = isReact19 ? InputV19 : InputV18;Q6: 如何调试使用这些特性的组件?
A: 使用React DevTools和标准调试技巧:
jsx
// 1. 添加displayName
ThemeContext.displayName = 'ThemeContext';
Input.displayName = 'Input';
// 2. 使用React DevTools
// - 查看Context当前值
// - 检查组件树结构
// - 追踪props和state变化
// 3. 添加调试日志
const ref = (element) => {
if (!element) {
console.log('Element unmounted');
return;
}
console.log('Element mounted:', element);
return () => {
console.log('Cleanup called for:', element);
};
};
// 4. 使用性能分析
import { Profiler } from 'react';
function App() {
return (
<Profiler
id="app"
onRender={(id, phase, actualDuration) => {
console.log(`${id} ${phase}: ${actualDuration}ms`);
}}
>
<YourComponent />
</Profiler>
);
}
// 5. 错误边界
class ErrorBoundary extends React.Component {
componentDidCatch(error, info) {
console.error('Error in ref/context:', error, info);
}
render() {
return this.props.children;
}
}Q7: 如何优化包含这些特性的大型应用?
A: 采用多种优化策略:
jsx
// 1. Context分离 - 避免不必要的重渲染
// ❌ 不好:所有值在一个Context
const AppContext = createContext({ theme, user, settings, cart });
// ✅ 好:按变化频率分离
const ThemeContext = createContext(theme); // 很少变化
const UserContext = createContext(user); // 偶尔变化
const CartContext = createContext(cart); // 频繁变化
// 2. 使用React.memo
const ExpensiveComponent = memo(({ data }) => {
return <div>{/* 复杂渲染 */}</div>;
});
// 3. 懒加载Context Provider
const ThemeProvider = lazy(() => import('./ThemeProvider'));
function App() {
return (
<Suspense fallback={<Loading />}>
<ThemeProvider>
<Content />
</ThemeProvider>
</Suspense>
);
}
// 4. ref callback优化 - 使用useCallback
function Component() {
const ref = useCallback((element) => {
if (!element) return;
const observer = new ResizeObserver(() => {});
observer.observe(element);
return () => observer.disconnect();
}, []); // 空依赖确保ref callback稳定
return <div ref={ref}>Content</div>;
}
// 5. Context选择器模式
function useThemeColor() {
const theme = useContext(ThemeContext);
return theme.colors.primary; // 只返回需要的部分
}总结
综合使用要点
React 19的ref和Context改进大大提升了代码的可读性、可维护性和开发效率:
1. ref作为prop的优势
- 更简洁的API:不再需要
forwardRef包装 - 更直观的代码:ref就像普通prop一样使用
- 更好的类型支持:TypeScript类型更自然
- 更少的样板代码:减少间接层和嵌套
2. ref callback清理的优势
- 更精确的清理时机:比useEffect更早触发
- 更简洁的代码:不需要useEffect包装
- 自动清理管理:返回函数即可
- 更少的bug:避免依赖数组问题
3. Context简化语法的优势
- 减少嵌套层级:直接使用Context作为Provider
- 更清晰的代码:少了
.Provider后缀 - 完全向后兼容:与旧语法可以共存
- 零性能开销:只是语法糖,运行时相同
最佳实践总结
什么时候使用ref as prop
jsx
// ✅ 需要从父组件访问DOM元素
function ParentComponent() {
const inputRef = useRef(null);
return <CustomInput ref={inputRef} />;
}
// ✅ 构建可复用的表单组件库
function Input({ ref, label, ...props }) {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
);
}
// ✅ 实现复杂的用户交互(焦点管理、滚动控制等)
function SearchComponent({ ref }) {
return <input ref={ref} placeholder="搜索..." />;
}什么时候使用ref callback清理
jsx
// ✅ 需要清理DOM事件监听器
const ref = (element) => {
if (!element) return;
const handler = () => console.log('click');
element.addEventListener('click', handler);
return () => element.removeEventListener('click', handler);
};
// ✅ 需要清理浏览器API(Observer、定时器等)
const ref = (element) => {
if (!element) return;
const observer = new IntersectionObserver(() => {});
observer.observe(element);
return () => observer.disconnect();
};
// ✅ 需要清理第三方库实例
const ref = (element) => {
if (!element) return;
const chart = new Chart(element, config);
return () => chart.destroy();
};什么时候使用Context简化语法
jsx
// ✅ 新项目,React 19环境
function App() {
return (
<ThemeContext value={theme}>
<UserContext value={user}>
<Content />
</UserContext>
</ThemeContext>
);
}
// ✅ 重构现有代码,逐步迁移
// 逐步将 <Context.Provider> 改为 <Context>
function NewFeature() {
return (
<NewContext value={value}>
<Content />
</NewContext>
);
}
// ⚠️ 需要兼容React 18
// 继续使用 <Context.Provider> 或提供兼容层架构设计建议
1. 组件库设计
jsx
// 所有可交互组件都支持ref
export function Button({ ref, children, ...props }) {
return <button ref={ref} {...props}>{children}</button>;
}
export function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
export function TextArea({ ref, ...props }) {
return <textarea ref={ref} {...props} />;
}
// 组合组件提供统一的ref接口
export function FormField({ ref, label, error, ...props }) {
return (
<div className="form-field">
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
);
}2. Context架构
jsx
// 按功能域分离Context
// ✅ 好的架构
const UIContext = createContext(uiState); // UI状态
const DataContext = createContext(data); // 数据
const AuthContext = createContext(auth); // 认证
const I18nContext = createContext(i18n); // 国际化
// ❌ 避免单一大Context
const AppContext = createContext({
ui, data, auth, i18n, settings, theme, ...
});
// 提供便捷的hooks
export function useUI() {
return useContext(UIContext);
}
export function useData() {
return useContext(DataContext);
}3. 清理逻辑模式
jsx
// 创建可复用的ref callback工厂函数
function useEventListener(eventName, handler) {
return useCallback((element) => {
if (!element) return;
element.addEventListener(eventName, handler);
return () => element.removeEventListener(eventName, handler);
}, [eventName, handler]);
}
// 使用
function Component() {
const handleClick = () => console.log('clicked');
const ref = useEventListener('click', handleClick);
return <div ref={ref}>Click me</div>;
}
// 组合多个清理逻辑
function useComposedRef(...refs) {
return useCallback((element) => {
if (!element) return;
const cleanups = refs.map(ref => {
if (typeof ref === 'function') {
return ref(element);
} else if (ref) {
ref.current = element;
}
}).filter(Boolean);
return () => {
cleanups.forEach(cleanup => cleanup());
};
}, refs);
}性能优化指南
1. 避免不必要的Context更新
jsx
// ❌ 每次渲染都创建新对象
function Provider({ children }) {
const [state, setState] = useState(initial);
return (
<Context value={{ state, setState }}>
{children}
</Context>
);
}
// ✅ 使用useMemo缓存value
function Provider({ children }) {
const [state, setState] = useState(initial);
const value = useMemo(
() => ({ state, setState }),
[state]
);
return (
<Context value={value}>
{children}
</Context>
);
}2. 稳定ref callback
jsx
// ❌ 每次渲染创建新函数
function Component({ data }) {
const ref = (element) => {
if (!element) return;
const observer = new ResizeObserver(() => {
console.log(data); // 依赖外部变量
});
observer.observe(element);
return () => observer.disconnect();
};
return <div ref={ref}>Content</div>;
}
// ✅ 使用useCallback稳定函数
function Component({ data }) {
const ref = useCallback((element) => {
if (!element) return;
const observer = new ResizeObserver(() => {
console.log(data);
});
observer.observe(element);
return () => observer.disconnect();
}, [data]); // 明确依赖
return <div ref={ref}>Content</div>;
}3. Context分离与懒加载
jsx
// 按更新频率分离
const StaticContext = createContext(staticData); // 不变
const SlowContext = createContext(slowData); // 很少变
const FastContext = createContext(fastData); // 频繁变
// 懒加载不常用的Provider
const AdminContext = lazy(() => import('./AdminContext'));
function App() {
return (
<StaticContext value={staticData}>
<SlowContext value={slowData}>
<FastContext value={fastData}>
<Suspense fallback={<Loading />}>
{isAdmin && (
<AdminContext value={adminData}>
<AdminPanel />
</AdminContext>
)}
</Suspense>
<RegularContent />
</FastContext>
</SlowContext>
</StaticContext>
);
}迁移路线图
阶段1:准备期(1-2周)
环境准备
- 升级React到19+
- 更新TypeScript类型
- 更新构建工具配置
团队培训
- 学习新特性
- 理解迁移策略
- 建立代码规范
评估现有代码
- 识别使用forwardRef的组件
- 识别使用useEffect做清理的地方
- 识别Context使用情况
阶段2:试点期(2-4周)
选择试点模块
- 新功能模块优先
- 影响面小的模块
- 非核心业务模块
迁移试点
- 应用新特性
- 编写测试
- 收集反馈
建立最佳实践
- 记录成功案例
- 识别常见问题
- 更新代码规范
阶段3:推广期(1-3个月)
核心组件库迁移
- 迁移所有基础组件
- 更新文档和示例
- 发布新版本
业务代码迁移
- 按模块逐步迁移
- 保持旧代码兼容
- 持续测试验证
优化与完善
- 性能优化
- 代码清理
- 文档完善
阶段4:完成期(持续)
完全迁移
- 移除所有旧代码
- 统一使用新特性
- 清理兼容层
持续改进
- 优化性能
- 改进开发体验
- 跟进React新版本
工具与资源
1. 开发工具
- React DevTools:查看Context值和组件树
- TypeScript:提供类型安全
- ESLint插件:强制最佳实践
- Prettier:统一代码风格
2. 测试工具
- React Testing Library:组件测试
- Jest:单元测试
- Playwright/Cypress:E2E测试
- React Profiler:性能分析
3. 学习资源
- React官方文档:最权威的学习资料
- React 19发布说明:了解新特性和Breaking Changes
- 社区博客:学习实践经验
- 开源项目:参考优秀实现
常见反模式
反模式1:过度使用ref
jsx
// ❌ 不应该用ref来做这些
function BadComponent() {
const countRef = useRef(0);
const dataRef = useRef([]);
// 应该用state
countRef.current++;
// 应该用state
dataRef.current.push(newItem);
return <div>Count: {countRef.current}</div>; // 不会重新渲染
}
// ✅ 正确使用ref
function GoodComponent() {
const [count, setCount] = useState(0); // 用state
const [data, setData] = useState([]); // 用state
const domRef = useRef(null); // ref用于DOM
useEffect(() => {
// 使用DOM ref
domRef.current.focus();
}, []);
return <input ref={domRef} />;
}反模式2:Context地狱
jsx
// ❌ 过度嵌套
function App() {
return (
<ThemeContext value={theme}>
<UserContext value={user}>
<SettingsContext value={settings}>
<I18nContext value={i18n}>
<RouterContext value={router}>
<DataContext value={data}>
<UIContext value={ui}>
<Content />
</UIContext>
</DataContext>
</RouterContext>
</I18nContext>
</SettingsContext>
</UserContext>
</ThemeContext>
);
}
// ✅ 提取Provider组合
function AppProviders({ children }) {
return (
<ThemeContext value={theme}>
<UserContext value={user}>
<SettingsContext value={settings}>
<I18nContext value={i18n}>
{children}
</I18nContext>
</SettingsContext>
</UserContext>
</ThemeContext>
);
}
function App() {
return (
<AppProviders>
<Content />
</AppProviders>
);
}反模式3:忘记清理
jsx
// ❌ 没有清理
function BadComponent() {
const ref = (element) => {
if (!element) return;
const observer = new MutationObserver(() => {});
observer.observe(element, { childList: true });
// 忘记返回清理函数!
};
return <div ref={ref}>Content</div>;
}
// ✅ 正确清理
function GoodComponent() {
const ref = (element) => {
if (!element) return;
const observer = new MutationObserver(() => {});
observer.observe(element, { childList: true });
return () => observer.disconnect();
};
return <div ref={ref}>Content</div>;
}结语
React 19的ref和Context改进标志着React框架的持续进化,这些改进不仅让代码更简洁,也让开发者的体验更好。通过本文的深入学习和实战案例,我们可以:
理解新特性的核心价值
- ref as prop简化了组件API
- ref callback清理提供了更好的资源管理
- Context简化语法减少了代码嵌套
掌握实际应用技巧
- 从简单案例到复杂系统
- 从单一特性到组合使用
- 从基础用法到高级模式
建立最佳实践
- 何时使用哪种特性
- 如何优化性能
- 如何避免常见错误
规划迁移策略
- 渐进式迁移
- 兼容性处理
- 团队协作
在实际项目中应用这些新特性时,要记住:
- 新特性是工具,不是目标
- 代码可读性和可维护性优先
- 性能优化要基于实际数据
- 团队协作和代码规范很重要
随着React生态的不断发展,保持学习和实践是每个React开发者的必修课。希望本文能够帮助你更好地理解和使用React 19的ref和Context改进,在实际项目中写出更优雅、更高效的代码。
注意事项
1. ref callback的执行时机
- ref callback在DOM挂载和卸载时执行
- 清理函数在元素卸载或ref改变时执行
- 避免在ref callback中直接更新state,可能导致循环渲染
2. Context值的稳定性
- Context value变化会导致所有消费者重新渲染
- 使用useMemo缓存Context value对象
- 将频繁变化的值和不变的值分离到不同的Context
3. ref as prop的限制
- ref在React 18中需要forwardRef
- 确保项目使用React 19+才能直接使用
- 库开发者需要考虑向后兼容性
4. 性能考虑
- ref callback每次重新创建会导致清理和重新初始化
- 使用useCallback稳定ref callback
- 避免在ref callback中执行耗时操作
5. 内存泄漏预防
- 确保所有监听器和订阅都有清理函数
- 清理函数中要释放所有资源引用
- 使用React DevTools Profiler检查内存使用
✅ ref作为prop简化组件API
✅ Context简化Provider语法,减少嵌套
✅ ref callback清理资源更精确
✅ 组合使用构建企业级应用最佳实践
✅ 渐进式迁移
✅ 保持代码一致性
✅ 充分的文档
✅ 全面的测试
✅ 性能优化
✅ 错误边界ref和Context改进让React 19代码更加简洁和强大!