React 高阶技巧实战

React 高阶技巧实战

一、自定义 Hooks

1.1 异步状态管理

typescript 复制代码
// hooks/useAsync.ts
import { useState, useCallback } from 'react';

interface AsyncState<T> {
  data: T | null;
  error: Error | null;
  loading: boolean;
}

interface UseAsyncReturn<T> extends AsyncState<T> {
  execute: () => Promise<void>;
  reset: () => void;
}

export function useAsync<T>(
  asyncFn: () => Promise<T>,
  options: { immediate?: boolean; onSuccess?: (data: T) => void; onError?: (e: Error) => void } = {}
): UseAsyncReturn<T> {
  const { immediate = true, onSuccess, onError } = options;
  
  const [state, setState] = useState<AsyncState<T>>({
    data: null,
    error: null,
    loading: false
  });

  const execute = useCallback(async () => {
    setState(prev => ({ ...prev, loading: true, error: null }));
    
    try {
      const result = await asyncFn();
      setState({ data: result, error: null, loading: false });
      onSuccess?.(result);
    } catch (err) {
      const error = err as Error;
      setState(prev => ({ ...prev, error, loading: false }));
      onError?.(error);
    }
  }, [asyncFn, onSuccess, onError]);

  const reset = useCallback(() => {
    setState({ data: null, error: null, loading: false });
  }, []);

  if (immediate) {
    // 使用 useEffect 会在组件首次挂载时执行
    const { useEffect } = require('react');
    useEffect(() => { execute(); }, []);
  }

  return { ...state, execute, reset };
}

// 使用
function UserProfile({ userId }: { userId: string }) {
  const { data: user, loading, error, execute: refresh } = useAsync(
    () => fetch(`/api/user/${userId}`).then(r => r.json()),
    { immediate: true }
  );

  if (loading) return <Skeleton />;
  if (error) return <ErrorFallback error={error} onRetry={refresh} />;
  
  return <div>{user.name}</div>;
}

1.2 自动保存表单

typescript 复制代码
// hooks/useAutoSave.ts
import { useRef, useEffect, useCallback } from 'react';
import { debounce } from 'lodash-es';

interface UseAutoSaveOptions<T> {
  data: T;
  onSave: (data: T) => Promise<void>;
  delay?: number;
  onSaveStart?: () => void;
  onSaveEnd?: (success: boolean) => void;
}

interface UseAutoSaveReturn {
  isSaving: boolean;
  lastSaved: Date | null;
  saveNow: () => Promise<void>;
}

export function useAutoSave<T>({
  data,
  onSave,
  delay = 3000,
  onSaveStart,
  onSaveEnd
}: UseAutoSaveOptions<T>): UseAutoSaveReturn {
  const [isSaving, setIsSaving] = useState(false);
  const [lastSaved, setLastSaved] = useState<Date | null>(null);
  const isFirstRender = useRef(true);

  const save = useCallback(async () => {
    setIsSaving(true);
    onSaveStart?.();
    
    try {
      await onSave(data);
      setLastSaved(new Date());
      onSaveEnd?.(true);
    } catch (e) {
      console.error('Auto-save failed:', e);
      onSaveEnd?.(false);
    } finally {
      setIsSaving(false);
    }
  }, [data, onSave, onSaveStart, onSaveEnd]);

  const debouncedSave = useMemo(
    () => debounce(save, delay),
    [save, delay]
  );

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }
    debouncedSave();
    return () => debouncedSave.cancel();
  }, [data, debouncedSave]);

  const saveNow = useCallback(async () => {
    debouncedSave.cancel();
    await save();
  }, [save, debouncedSave]);

  return { isSaving, lastSaved, saveNow };
}

// 使用
function ArticleEditor() {
  const [form, setForm] = useState({ title: '', content: '' });
  const [status, setStatus] = useState('');

  const { isSaving, lastSaved } = useAutoSave({
    data: form,
    onSave: (data) => api.saveDraft(data),
    delay: 5000,
    onSaveStart: () => setStatus('保存中...'),
    onSaveEnd: (success) => setStatus(success ? '已保存' : '保存失败')
  });

  return (
    <div>
      <input value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} />
      <textarea value={form.content} onChange={e => setForm({ ...form, content: e.target.value })} />
      <span>{status}</span>
      {isSaving && <Spinner />}
    </div>
  );
}

1.3 无限滚动

typescript 复制代码
// hooks/useInfiniteScroll.ts
import { useState, useCallback, useRef, useEffect } from 'react';

interface UseInfiniteScrollOptions {
  fetchMore: (page: number) => Promise<{ data: any[]; hasMore: boolean }>;
  threshold?: number;
  immediate?: boolean;
}

interface UseInfiniteScrollReturn {
  items: any[];
  loading: boolean;
  finished: boolean;
  error: Error | null;
  containerRef: React.RefObject<HTMLDivElement>;
  reset: () => void;
}

export function useInfiniteScroll({
  fetchMore,
  threshold = 100,
  immediate = true
}: UseInfiniteScrollOptions): UseInfiniteScrollReturn {
  const containerRef = useRef<HTMLDivElement>(null);
  const [items, setItems] = useState<any[]>([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [finished, setFinished] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const isFetchingRef = useRef(false);

  const loadMore = useCallback(async () => {
    if (loading || finished || isFetchingRef.current) return;
    
    isFetchingRef.current = true;
    setLoading(true);
    setError(null);

    try {
      const result = await fetchMore(page);
      setItems(prev => [...prev, ...result.data]);
      setFinished(!result.hasMore);
      if (result.hasMore) setPage(p => p + 1);
    } catch (e) {
      setError(e as Error);
    } finally {
      setLoading(false);
      isFetchingRef.current = false;
    }
  }, [fetchMore, page, loading, finished]);

  const handleScroll = useCallback(() => {
    const container = containerRef.current;
    if (!container) return;

    const { scrollTop, scrollHeight, clientHeight } = container;
    if (scrollHeight - scrollTop - clientHeight <= threshold) {
      loadMore();
    }
  }, [loadMore, threshold]);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    container.addEventListener('scroll', handleScroll);
    return () => container.removeEventListener('scroll', handleScroll);
  }, [handleScroll]);

  useEffect(() => {
    if (immediate && !finished) {
      loadMore();
    }
  }, []);

  const reset = useCallback(() => {
    setItems([]);
    setPage(1);
    setFinished(false);
    setError(null);
    loadMore();
  }, [loadMore]);

  return { items, loading, finished, error, containerRef, reset };
}

// 使用
function FeedList() {
  const fetchPage = async (page: number) => {
    const res = await api.getPosts(page);
    return { data: res.items, hasMore: res.hasMore };
  };

  const { items, containerRef, loading, finished } = useInfiniteScroll({ fetchMore: fetchPage });

  return (
    <div ref={containerRef} style={{ height: '400px', overflow: 'auto' }}>
      {items.map(item => <PostCard key={item.id} data={item} />)}
      {loading && <LoadingSpinner />}
      {finished && <div>没有更多了</div>}
    </div>
  );
}

二、Render Props 与组件组合

2.1 动态表格列

tsx 复制代码
// components/DataGrid.tsx
import React from 'react';

interface Column<T> {
  key: keyof T | string;
  title: string;
  width?: number;
  render?: (row: T, index: number) => React.ReactNode;
}

interface DataGridProps<T> {
  data: T[];
  columns: Column<T>[];
  rowKey?: keyof T;
  emptyText?: string;
}

export function DataGrid<T extends Record<string, any>>({
  data,
  columns,
  rowKey = 'id' as keyof T,
  emptyText = '暂无数据'
}: DataGridProps<T>) {
  return (
    <table className="data-grid">
      <thead>
        <tr>
          {columns.map(col => (
            <th key={String(col.key)} style={{ width: col.width }}>
              {col.title}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.length === 0 ? (
          <tr>
            <td colSpan={columns.length} className="empty-cell">
              {emptyText}
            </td>
          </tr>
        ) : (
          data.map((row, idx) => (
            <tr key={String(row[rowKey])}>
              {columns.map(col => (
                <td key={String(col.key)}>
                  {col.render
                    ? col.render(row, idx)
                    : (row as any)[col.key as string]}
                </td>
              ))}
            </tr>
          ))
        )}
      </tbody>
    </table>
  );
}

// 使用
function UserTable() {
  return (
    <DataGrid
      data={users}
      rowKey="id"
      columns={[
        { key: 'name', title: '姓名' },
        { key: 'status', title: '状态', render: (row) => (
          <span className={row.status === 1 ? 'tag-success' : 'tag-error'}>
            {row.status === 1 ? '启用' : '禁用'}
          </span>
        )},
        { key: 'action', title: '操作', render: (row) => (
          <button onClick={() => handleEdit(row)}>编辑</button>
        )}
      ]}
    />
  );
}

2.2 递归菜单组件

tsx 复制代码
// components/RecursiveMenu.tsx
import React, { useState } from 'react';

interface MenuItem {
  id: string;
  label: string;
  icon?: string;
  children?: MenuItem[];
  onClick?: () => void;
}

interface MenuItemProps {
  item: MenuItem;
  level?: number;
}

function MenuItemComponent({ item, level = 0 }: MenuItemProps) {
  const [expanded, setExpanded] = useState(false);
  const hasChildren = item.children && item.children.length > 0;

  const handleClick = () => {
    if (hasChildren) {
      setExpanded(!expanded);
    } else {
      item.onClick?.();
    }
  };

  return (
    <div className="menu-item-wrapper">
      <div
        className="menu-item"
        style={{ paddingLeft: level * 16 + 'px' }}
        onClick={handleClick}
      >
        {item.icon && <i className={item.icon} />}
        <span>{item.label}</span>
        {hasChildren && (
          <i className={expanded ? 'icon-arrow-down' : 'icon-arrow-right'} />
        )}
      </div>
      {hasChildren && expanded && (
        <div className="menu-children">
          {item.children!.map(child => (
            <MenuItemComponent key={child.id} item={child} level={level + 1} />
          ))}
        </div>
      )}
    </div>
  );
}

interface RecursiveMenuProps {
  items: MenuItem[];
}

export function RecursiveMenu({ items }: RecursiveMenuProps) {
  return (
    <div className="recursive-menu">
      {items.map(item => (
        <MenuItemComponent key={item.id} item={item} />
      ))}
    </div>
  );
}

// 使用
function App() {
  const menuData: MenuItem[] = [
    {
      id: '1',
      label: '系统管理',
      icon: 'icon-settings',
      children: [
        { id: '1-1', label: '用户管理', onClick: () => navigate('/users') },
        { id: '1-2', label: '角色管理', onClick: () => navigate('/roles') }
      ]
    },
    { id: '2', label: '订单管理', onClick: () => navigate('/orders') }
  ];

  return <RecursiveMenu items={menuData} />;
}

2.3 Render Props 模式

tsx 复制代码
// components/MouseTracker.tsx
import React, { useState, useCallback, useRef, useEffect } from 'react';

interface Position {
  x: number;
  y: number;
}

interface MouseTrackerProps {
  render: (position: Position) => React.ReactNode;
}

export function MouseTracker({ render }: MouseTrackerProps) {
  const [position, setPosition] = useState<Position>({ x: 0, y: 0 });

  const handleMouseMove = useCallback((e: MouseEvent) => {
    setPosition({ x: e.clientX, y: e.clientY });
  }, []);

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, [handleMouseMove]);

  return <>{render(position)}</>;
}

// 使用
function App() {
  return (
    <MouseTracker render={({ x, y }) => (
      <div>
        鼠标位置: ({x}, {y})
        <FollowCursor x={x} y={y} />
      </div>
    )} />
  );
}

三、Context 与状态管理

3.1 类型安全的 Context

typescript 复制代码
// context/FormContext.tsx
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';

interface FormContextValue {
  values: Record<string, any>;
  errors: Record<string, string>;
  touched: Record<string, boolean>;
  setValue: (field: string, value: any) => void;
  setError: (field: string, error: string) => void;
  setTouched: (field: string) => void;
  validate: () => boolean;
  reset: () => void;
}

const FormContext = createContext<FormContextValue | null>(null);

interface FormProviderProps {
  initialValues: Record<string, any>;
  validationRules?: Record<string, (value: any) => string | null>;
  onSubmit?: (values: Record<string, any>) => Promise<void>;
  children: ReactNode;
}

export function FormProvider({
  initialValues,
  validationRules = {},
  onSubmit,
  children
}: FormProviderProps) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [touched, setTouched] = useState<Record<string, boolean>>({});

  const setValue = useCallback((field: string, value: any) => {
    setValues(prev => ({ ...prev, [field]: value }));
    
    if (touched[field] && validationRules[field]) {
      const error = validationRules[field](value);
      setErrors(prev => ({ ...prev, [field]: error || '' }));
    }
  }, [touched, validationRules]);

  const setError = useCallback((field: string, error: string) => {
    setErrors(prev => ({ ...prev, [field]: error }));
  }, []);

  const setTouched = useCallback((field: string) => {
    setTouched(prev => ({ ...prev, [field]: true }));
    
    if (validationRules[field]) {
      const error = validationRules[field](values[field]);
      setErrors(prev => ({ ...prev, [field]: error || '' }));
    }
  }, [values, validationRules]);

  const validate = useCallback((): boolean => {
    const newErrors: Record<string, string> = {};
    let isValid = true;

    Object.entries(validationRules).forEach(([field, rule]) => {
      const error = rule(values[field]);
      if (error) {
        newErrors[field] = error;
        isValid = false;
      }
    });

    setErrors(newErrors);
    return isValid;
  }, [values, validationRules]);

  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    const allTouched = Object.keys(validationRules).reduce(
      (acc, key) => ({ ...acc, [key]: true }),
      {}
    );
    setTouched(allTouched);

    if (!validate()) return;
    await onSubmit?.(values);
  };

  const value: FormContextValue = {
    values, errors, touched,
    setValue, setError, setTouched,
    validate, reset
  };

  return (
    <FormContext.Provider value={value}>
      <form onSubmit={handleSubmit}>{children}</form>
    </FormContext.Provider>
  );
}

// 消费 Context
export function useFormContext() {
  const context = useContext(FormContext);
  if (!context) {
    throw new Error('useFormContext must be used within FormProvider');
  }
  return context;
}

// 使用
function LoginForm() {
  return (
    <FormProvider
      initialValues={{ email: '', password: '' }}
      validationRules={{
        email: (v) => /^\S+@\S+$/.test(v) ? null : '邮箱格式不正确',
        password: (v) => v.length >= 6 ? null : '密码至少6位'
      }}
      onSubmit={async (values) => { await api.login(values); }}
    >
      <FormField name="email" label="邮箱">
        <FormInput name="email" type="email" />
        <FormError name="email" />
      </FormField>
      <FormField name="password" label="密码">
        <FormInput name="password" type="password" />
        <FormError name="password" />
      </FormField>
      <SubmitButton>登录</SubmitButton>
    </FormProvider>
  );
}

3.2 跨层级状态共享(轻量级)

typescript 复制代码
// hooks/useSharedState.ts
import { useState, useCallback, useRef } from 'react';

const store = new Map<string, { value: any; subscribers: Set<() => void> }>();

export function useSharedState<T>(key: string, initialValue: T): [T, (value: T) => void] {
  if (!store.has(key)) {
    store.set(key, { value: initialValue, subscribers: new Set() });
  }

  const [state, setState] = useState<T>(() => store.get(key)!.value);

  const entry = store.get(key)!;
  
  const wrappedSetState = useCallback((value: T) => {
    entry.value = value;
    setState(value);
    entry.subscribers.forEach(callback => callback());
  }, []);

  const wrapperSetState = useCallback((value: T | ((prev: T) => T)) => {
    const resolvedValue = typeof value === 'function'
      ? (value as (prev: T) => T)(entry.value)
      : value;
    wrappedSetState(resolvedValue);
  }, [wrappedSetState]);

  return [state, wrapperSetState];
}

// 组件 A
function ComponentA() {
  const [count, setCount] = useSharedState('globalCount', 0);
  return <button onClick={() => setCount(count + 1)}>A: {count}</button>;
}

// 组件 B(任意层级)
function ComponentB() {
  const [count] = useSharedState('globalCount', 0);
  return <span>B: {count}</span>;
}

3.3 状态机模式

typescript 复制代码
// hooks/useStateMachine.ts
type StateMachine<S, A> = {
  currentState: S;
  dispatch: (action: A) => void;
};

export function useStateMachine<S extends string, A extends string>(
  config: Record<S, Record<A, { nextState: S; sideEffect?: () => void }>>
) {
  const [currentState, setCurrentState] = useState<S>(Object.keys(config)[0] as S);
  const sideEffectRef = useRef<(() => void) | null>(null);

  const dispatch = useCallback((action: A) => {
    const stateConfig = config[currentState];
    if (!stateConfig || !stateConfig[action]) return;

    const { nextState, sideEffect } = stateConfig[action];
    setCurrentState(nextState);
    
    if (sideEffect) {
      sideEffectRef.current = sideEffect;
      sideEffect();
    }
  }, [currentState, config]);

  return { currentState, dispatch } as StateMachine<S, A>;
}

// 使用
interface FetchStates {
  idle: { fetch: { nextState: 'loading' } };
  loading: { succeed: { nextState: 'success'; sideEffect: () => void }; fail: { nextState: 'error' } };
  success: { refresh: { nextState: 'loading' } };
  error: { retry: { nextState: 'loading' } };
}

function useFetch<T>(fetchFn: () => Promise<T>) {
  const { currentState, dispatch } = useStateMachine<FetchStates>({
    idle: { fetch: { nextState: 'loading' } },
    loading: {
      succeed: { nextState: 'success', sideEffect: () => console.log('Fetched!') },
      fail: { nextState: 'error' }
    },
    success: { refresh: { nextState: 'loading' } },
    error: { retry: { nextState: 'loading' } }
  });

  const trigger = async () => {
    dispatch('fetch');
    try {
      await fetchFn();
      dispatch('succeed');
    } catch {
      dispatch('fail');
    }
  };

  return { state: currentState, trigger, retry: () => dispatch('retry') };
}

四、性能优化技巧

4.1 虚拟列表

tsx 复制代码
// components/VirtualList.tsx
import React, { useState, useRef, useCallback, useEffect, ReactNode } from 'react';

interface VirtualListProps<T> {
  items: T[];
  itemHeight: number;
  containerHeight: number;
  renderItem: (item: T, index: number) => ReactNode;
  overscan?: number;
}

export function VirtualList<T>({
  items,
  itemHeight,
  containerHeight,
  renderItem,
  overscan = 5
}: VirtualListProps<T>) {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);

  const totalHeight = items.length * itemHeight;
  
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
  const visibleCount = Math.ceil(containerHeight / itemHeight);
  const endIndex = Math.min(items.length, startIndex + visibleCount + overscan * 2);

  const visibleItems = items.slice(startIndex, endIndex);
  const offsetY = startIndex * itemHeight;

  const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
    setScrollTop(e.currentTarget.scrollTop);
  }, []);

  return (
    <div
      ref={containerRef}
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div key={startIndex + index} style={{ height: itemHeight }}>
              {renderItem(item, startIndex + index)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// 使用
function App() {
  const items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`
  }));

  return (
    <VirtualList
      items={items}
      itemHeight={50}
      containerHeight={400}
      renderItem={(item) => <div>{item.name}</div>}
    />
  );
}

4.2 useMemo 与 useCallback 策略

tsx 复制代码
// 组件中的优化实践
function ProductList({ categoryId }: { categoryId: string }) {
  const [searchText, setSearchText] = useState('');
  const [sortKey, setSortKey] = useState<'name' | 'price'>('name');

  // 基础数据
  const allProducts = useQuery(['products', categoryId], () => api.getProducts(categoryId));

  // 筛选逻辑
  const filteredProducts = useMemo(() => {
    if (!searchText) return allProducts;
    return allProducts.filter(p => 
      p.name.toLowerCase().includes(searchText.toLowerCase())
    );
  }, [allProducts, searchText]);

  // 排序逻辑(独立计算)
  const sortedProducts = useMemo(() => {
    return [...filteredProducts].sort((a, b) => {
      if (sortKey === 'price') return a.price - b.price;
      return a.name.localeCompare(b.name);
    });
  }, [filteredProducts, sortKey]);

  // 事件处理
  const handleItemClick = useCallback((productId: string) => {
    navigate(`/product/${productId}`);
  }, []);

  const handleAddToCart = useCallback((productId: string, quantity: number) => {
    dispatch(addToCart({ productId, quantity }));
  }, []);

  // 避免过度 memo(简单计算不需要)
  const totalCount = allProducts.length; // 不需要 useMemo

  return (
    <div>
      <SearchInput value={searchText} onChange={setSearchText} />
      <SortButtons value={sortKey} onChange={setSortKey} />
      
      {sortedProducts.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onClick={() => handleItemClick(product.id)}
          onAddToCart={(qty) => handleAddToCart(product.id, qty)}
        />
      ))}
    </div>
  );
}

4.3 React.lazy 与 Suspense

tsx 复制代码
// routes/index.tsx
import React, { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Users = lazy(() => import('./pages/Users'));
const Settings = lazy(() => import('./pages/Settings'));
const Reports = lazy(() => import('./pages/Reports'));

function LoadingFallback() {
  return (
    <div className="loading-skeleton">
      <div className="skeleton-header" />
      <div className="skeleton-content" />
      <div className="skeleton-sidebar" />
    </div>
  );
}

function ErrorBoundary({ children, fallback }: { children: React.ReactNode; fallback: React.ReactNode }) {
  const [hasError, setHasError] = useState(false);

  return hasError ? <>{fallback}</> : <>{children}</>;
}

function AppRouter() {
  const [routes] = useState([
    { path: '/dashboard', component: Dashboard },
    { path: '/users', component: Users },
    { path: '/settings', component: Settings },
    { path: '/reports', component: Reports }
  ]);

  return (
    <Routes>
      {routes.map(route => (
        <Route
          key={route.path}
          path={route.path}
          element={
            <ErrorBoundary fallback={<div>加载失败</div>}>
              <Suspense fallback={<LoadingFallback />}>
                <route.component />
              </Suspense>
            </ErrorBoundary>
          }
        />
      ))}
    </Routes>
  );
}

4.4 ref 回调模式

tsx 复制代码
// 稳定的 ref 回调
function RefCallbackDemo() {
  const [showTip, setShowTip] = useState(false);
  
  // 使用 useCallback 确保回调稳定,避免重复渲染
  const buttonRef = useCallback((node: HTMLButtonElement | null) => {
    if (node) {
      node.dataset.initialized = 'true';
      console.log('Button mounted');
    }
  }, []);

  return (
    <div>
      <button ref={buttonRef} onClick={() => setShowTip(!showTip)}>
        Toggle Tooltip
      </button>
      {showTip && <Tooltip>提示信息</Tooltip>}
    </div>
  );
}

// 多个 ref 的合并
function MultiRefDemo() {
  const localRef = useRef<HTMLDivElement>(null);
  const [mounted, setMounted] = useState(false);

  const setRefs = useCallback((node: HTMLDivElement | null) => {
    localRef.current = node;
    if (node) {
      setMounted(true);
      node.classList.add('mounted');
    }
  }, []);

  return <div ref={setRefs}>{mounted && 'Ready'}</div>;
}

五、TypeScript 高级模式

5.1 Props 类型推导

typescript 复制代码
// components/types.ts
import React from 'react';

// 基础 Props
interface BaseProps {
  className?: string;
  style?: React.CSSProperties;
  'data-testid'?: string;
}

// Props 组合
interface ButtonProps extends BaseProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  loading?: boolean;
  onClick?: (e: React.MouseEvent) => void;
  children: React.ReactNode;
  icon?: React.ReactNode;
}

// Props 排除/提取
type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
  value: string;
  onChange: (value: string) => void;
  error?: string;
};

// 泛型组件
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  emptyText?: string;
}

export function List<T>({ items, renderItem, keyExtractor, emptyText = '暂无数据' }: ListProps<T>) {
  if (items.length === 0) return <div className="empty">{emptyText}</div>;
  
  return (
    <div className="list">
      {items.map((item, index) => (
        <div key={keyExtractor(item)}>{renderItem(item, index)}</div>
      ))}
    </div>
  );
}

// 使用泛型
function App() {
  const users = [{ id: 1, name: 'Tom' }, { id: 2, name: 'Jerry' }];
  
  return (
    <List
      items={users}
      keyExtractor={u => u.id}
      renderItem={(user, index) => <div>{index + 1}. {user.name}</div>}
    />
  );
}

5.2 事件类型安全

typescript 复制代码
// 组件事件定义
interface ButtonEmits {
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
  onMouseEnter: (e: React.MouseEvent<HTMLButtonElement>) => void;
  onMouseLeave: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

// 联合类型事件
type ToggleEvents = 
  | { type: 'toggle'; value: boolean }
  | { type: 'change'; value: string }
  | { type: 'error'; error: Error };

function useToggleReducer() {
  return useReducer((state: any, event: ToggleEvents) => {
    switch (event.type) {
      case 'toggle':
        return { ...state, isOpen: event.value };
      case 'change':
        return { ...state, value: event.value };
      case 'error':
        return { ...state, error: event.error };
    }
  }, { isOpen: false, value: '' });
}

5.3 响应式 Hooks 类型

typescript 复制代码
// 自定义 Hooks 的类型约束
interface UseToggleReturn {
  value: boolean;
  toggle: () => void;
  setTrue: () => void;
  setFalse: () => void;
}

function useToggle(initialValue = false): UseToggleReturn {
  const [value, setValue] = useState(initialValue);
  
  return {
    value,
    toggle: useCallback(() => setValue(v => !v), []),
    setTrue: useCallback(() => setValue(true), []),
    setFalse: useCallback(() => setValue(false), [])
  };
}

// 泛型 Hook
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 {
      return initialValue;
    }
  });

  const setValue = useCallback((value: T | ((val: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  }, [key, storedValue]);

  return [storedValue, setValue] as const;
}

// 使用
function App() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [isDark, toggle, setTrue, setFalse] = useToggle(false);
  
  // 类型自动推导
  // theme: 'light' | 'dark'
  // isDark: boolean
}

六、React Router 技巧

6.1 路由守卫组合

tsx 复制代码
// hooks/useAuth.ts
import { useContext } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { AuthContext } from '@/context/AuthContext';

interface ProtectedRouteProps {
  children: React.ReactNode;
  requiredPermissions?: string[];
}

export function ProtectedRoute({ children, requiredPermissions = [] }: ProtectedRouteProps) {
  const { user, hasPermissions, isAuthenticated } = useContext(AuthContext);
  const location = useLocation();

  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (requiredPermissions.length > 0 && !hasPermissions(requiredPermissions)) {
    return <Navigate to="/403" replace />;
  }

  return <>{children}</>;
}

// 使用
function App() {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route path="/403" element={<ForbiddenPage />} />
      
      <Route
        path="/admin"
        element={
          <ProtectedRoute requiredPermissions={['admin:access']}>
            <AdminLayout />
          </ProtectedRoute>
        }
      >
        <Route path="users" element={<UsersPage />} />
        <Route path="settings" element={<SettingsPage />} />
      </Route>
    </Routes>
  );
}

6.2 路由缓存控制

tsx 复制代码
// components/RouteCache.tsx
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';

interface CacheContextValue {
  cachedRoutes: Set<string>;
  addCache: (name: string) => void;
  removeCache: (name: string) => void;
  clearCache: () => void;
}

const CacheContext = createContext<CacheContextValue | null>(null);

export function CacheProvider({ children }: { children: ReactNode }) {
  const [cachedRoutes, setCachedRoutes] = useState<Set<string>>(new Set());

  const addCache = useCallback((name: string) => {
    setCachedRoutes(prev => new Set([...prev, name]));
  }, []);

  const removeCache = useCallback((name: string) => {
    setCachedRoutes(prev => {
      const next = new Set(prev);
      next.delete(name);
      return next;
    });
  }, []);

  const clearCache = useCallback(() => {
    setCachedRoutes(new Set());
  }, []);

  return (
    <CacheContext.Provider value={{ cachedRoutes, addCache, removeCache, clearCache }}>
      {children}
    </CacheContext.Provider>
  );
}

export function RouteView() {
  const { cachedRoutes } = useContext(CacheContext)!;
  const location = useLocation();

  // 根据路由 meta 管理缓存
  const routeConfig = useRouteConfig(location.pathname);
  if (routeConfig?.meta?.keepAlive && !cachedRoutes.has(location.pathname)) {
    // 添加到缓存列表
  }

  return (
    <>
      {Array.from(cachedRoutes).map(path => (
        <div key={path} style={{ display: 'none' }}>
          <RouteComponent path={path} />
        </div>
      ))}
      <div>
        <RouteComponent path={location.pathname} />
      </div>
    </>
  );
}

6.3 滚动行为控制

tsx 复制代码
// hooks/useScrollReset.ts
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export function useScrollReset() {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo({ top: 0, behavior: 'smooth' });
  }, [pathname]);
}

// 使用
function App() {
  return (
    <>
      <useScrollReset />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </>
  );
}

七、状态管理进阶(Zustand)

7.1 Store 模块化

typescript 复制代码
// stores/userStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { useShallow } from 'zustand/react/shallow';

interface UserInfo {
  id: string;
  name: string;
  email: string;
}

interface UserState {
  token: string | null;
  userInfo: UserInfo | null;
  isLoggedIn: boolean;
  permissions: string[];
}

interface UserActions {
  login: (credentials: { email: string; password: string }) => Promise<void>;
  logout: () => void;
  fetchUserInfo: () => Promise<void>;
  hasPermission: (permission: string) => boolean;
}

type UserStore = UserState & UserActions;

export const useUserStore = create<UserStore>()(
  devtools(
    persist(
      (set, get) => ({
        token: null,
        userInfo: null,
        isLoggedIn: false,
        permissions: [],

        login: async ({ email, password }) => {
          const response = await api.login({ email, password });
          set({
            token: response.token,
            userInfo: response.user,
            isLoggedIn: true,
            permissions: response.user.permissions
          });
        },

        logout: () => {
          set({
            token: null,
            userInfo: null,
            isLoggedIn: false,
            permissions: []
          });
        },

        fetchUserInfo: async () => {
          const user = await api.getUserInfo();
          set({ userInfo: user, permissions: user.permissions });
        },

        hasPermission: (permission: string) => {
          return get().permissions.includes(permission);
        }
      }),
      { name: 'user-storage', partialize: (state) => ({ token: state.token }) }
    )
  )
);

// 使用
function UserMenu() {
  const { userInfo, logout, isLoggedIn } = useUserStore();
  
  if (!isLoggedIn) return <LoginLink />;
  
  return (
    <div>
      <span>{userInfo?.name}</span>
      <button onClick={logout}>退出</button>
    </div>
  );
}

// 选择性订阅(性能优化)
function Badge() {
  const permissions = useUserStore(state => state.permissions);
  const isAdmin = useUserStore(state => state.permissions.includes('admin'));
  
  return isAdmin ? <AdminBadge /> : null;
}

// 使用 shallow 比较
function PermissionsList() {
  const { permissions, role } = useUserStore(
    useShallow(state => ({ permissions: state.permissions, role: state.role }))
  );
  
  return <div>{permissions.join(', ')}</div>;
}

7.2 Store 组合

typescript 复制代码
// stores/useAppStore.ts
import { create } from 'zustand';
import { useUserStore } from './userStore';
import { usePermissionStore } from './permissionStore';

interface AppState {
  theme: 'light' | 'dark';
  sidebarCollapsed: boolean;
}

interface AppActions {
  setTheme: (theme: 'light' | 'dark') => void;
  toggleSidebar: () => void;
  canAccess: (route: string) => boolean;
}

export const useAppStore = create<AppState & AppActions>((set, get) => ({
  theme: 'light',
  sidebarCollapsed: false,

  setTheme: (theme) => set({ theme }),
  toggleSidebar: () => set(state => ({ sidebarCollapsed: !state.sidebarCollapsed })),

  canAccess: (route) => {
    const user = useUserStore.getState();
    const permission = usePermissionStore.getState();
    
    if (!user.isLoggedIn) return false;
    return permission.hasRoute(route);
  }
}));

// 跨 Store 订阅
export function useSyncUserPermissions() {
  const user = useUserStore();
  const permission = usePermissionStore();

  useEffect(() => {
    if (user.isLoggedIn && user.userInfo) {
      permission.loadPermissions(user.userInfo.role);
    }
  }, [user.isLoggedIn, user.userInfo]);
}

八、React 18 新特性

8.1 useTransition

tsx 复制代码
// components/SearchWithTransition.tsx
import React, { useState, useTransition } from 'react';

function SearchResults({ query }: { query: string }) {
  const results = useMemo(() => {
    // 大量数据筛选
    return allData.filter(item => item.name.includes(query));
  }, [query]);

  return (
    <ul>
      {results.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

function SearchPage() {
  const [inputValue, setInputValue] = useState('');
  const [query, setQuery] = useState('');
  
  // 将状态更新标记为非紧急
  const [isPending, startTransition] = useTransition();

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setInputValue(value);

    // 标记为 transition,避免阻塞 UI
    startTransition(() => {
      setQuery(value);
    });
  };

  return (
    <div>
      <input value={inputValue} onChange={handleInputChange} />
      {isPending ? <Spinner /> : <SearchResults query={query} />}
    </div>
  );
}

8.2 useDeferredValue

tsx 复制代码
// components/FilteredList.tsx
import React, { useState, useDeferredValue } from 'react';

function FilteredList({ filter }: { filter: string }) {
  const deferredFilter = useDeferredValue(filter);

  const items = useMemo(() => {
    return heavyList.filter(item => 
      item.name.includes(deferredFilter)
    );
  }, [heavyList, deferredFilter]);

  const isStale = filter !== deferredFilter;

  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      {items.map(item => <ListItem key={item.id} item={item} />)}
    </div>
  );
}

function App() {
  const [filter, setFilter] = useState('');
  
  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      <FilteredList filter={filter} />
    </div>
  );
}

8.3 自动批处理

tsx 复制代码
// 行为对比
function OldBehavior() {
  const [state, setState] = useState({ count: 0 });

  const handleClick = async () => {
    // 以前:每个 setState 都会触发一次渲染
    const res = await fetch('/api');
    setState({ count: 1 }); // 渲染 1
    setState({ count: 2 }); // 渲染 2
    setError(null);         // 渲染 3
  };

  return null;
}

function NewBehavior() {
  const [state, setState] = useState({ count: 0, error: null });

  const handleClick = async () => {
    // React 18:所有更新自动批处理,只触发一次渲染
    const res = await fetch('/api');
    setState(prev => ({ ...prev, count: 1 }));
    setState(prev => ({ ...prev, count: 2 }));
    setState(prev => ({ ...prev, error: null }));
    // 只会触发一次渲染!
  };

  return null;
}

8.4 Suspense 服务端渲染

tsx 复制代码
// SSR 组件
import { lazy, Suspense } from 'react';

const ProductDetails = lazy(() => import('./ProductDetails'));

function ProductPage({ productId }: { productId: string }) {
  const product = useQuery(['product', productId], () => api.getProduct(productId));

  return (
    <div>
      <h1>{product.name}</h1>
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetails productId={productId} />
      </Suspense>
    </div>
  );
}

// 流式渲染
import { renderToPipeableStream } from 'react-dom/server';

app.get('/product/:id', async (req, res) => {
  const stream = renderToPipeableStream(<ProductPage productId={req.params.id} />, {
    bootstrapScripts: ['/main.js'],
    onShellReady() {
      stream.pipe(res);
    },
    onError(error) {
      console.error(error);
    }
  });
});

九、更多自定义 Hooks

9.1 事件监听 Hook

typescript 复制代码
// hooks/useEventListener.ts
import { useEffect, useRef } from 'react';

type EventListener<K extends keyof WindowEventMap> = (event: WindowEventMap[K]) => void;

export function useEventListener<K extends keyof WindowEventMap>(
  event: K,
  handler: EventListener<K>,
  target?: EventTarget | null,
  options?: boolean | AddEventListenerOptions
): void {
  const handlerRef = useRef(handler);
  
  useEffect(() => {
    handlerRef.current = handler;
  }, [handler]);

  useEffect(() => {
    const element = target ?? window;
    
    const listener: EventListener = (e) => handlerRef.current(e as any);
    
    element.addEventListener(event, listener, options);
    
    return () => element.removeEventListener(event, listener);
  }, [event, target, options]);
}

// 使用 - 监听窗口大小变化
function ResponsiveComponent() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEventListener('resize', () => {
    setSize({
      width: window.innerWidth,
      height: window.innerHeight
    });
  });

  return <div>{size.width} x {size.height}</div>;
}

// 使用 - 键盘快捷键
function useKeyboardShortcut(keys: string[], callback: () => void) {
  useEventListener('keydown', (e) => {
    if (keys.includes(e.key)) {
      e.preventDefault();
      callback();
    }
  });

  // 使用
  // useKeyboardShortcut(['s', 'S'], () => handleSave());
}

9.2 媒体查询 Hook

typescript 复制代码
// hooks/useMediaQuery.ts
import { useState, useEffect } from 'react';

interface UseMediaQueryOptions {
  initialMatch?: boolean;
}

export function useMediaQuery(
  query: string,
  options?: UseMediaQueryOptions
): boolean {
  const { initialMatch = false } = options ?? {};
  const [matches, setMatches] = useState(initialMatch);

  useEffect(() => {
    const mediaQuery = window.matchMedia(query);
    
    setMatches(mediaQuery.matches);

    const handler = (event: MediaQueryListEvent) => setMatches(event.matches);
    
    mediaQuery.addEventListener('change', handler);
    
    return () => mediaQuery.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

// 预设断点
const breakpoints = {
  sm: '(min-width: 640px)',
  md: '(min-width: 768px)',
  lg: '(min-width: 1024px)',
  xl: '(min-width: 1280px)',
  '2xl': '(min-width: 1536px)'
};

function useBreakpoints() {
  return Object.entries(breakpoints).reduce(
    (acc, [key, value]) => ({
      ...acc,
      [key]: useMediaQuery(value)
    }),
    {} as Record<keyof typeof breakpoints, boolean>
  );
}

// 使用
function ResponsiveLayout() {
  const isMobile = !useMediaQuery('(min-width: 768px)');
  const isTablet = useMediaQuery('(min-width: 768px)') && !useMediaQuery('(min-width: 1024px)');
  const isDesktop = useMediaQuery('(min-width: 1024px)');

  const bp = useBreakpoints();
  // bp.sm, bp.md, bp.lg, bp.xl, bp['2xl']

  return (
    <>
      {isMobile && <MobileNav />}
      {(isTablet || isDesktop) && <Sidebar />}
      {bp.xl && <AdvancedFeatures />}
    </>
  );
}

9.3 本地存储 Hook

typescript 复制代码
// hooks/useLocalStorage.ts
import { useState, useCallback, useRef } from 'react';
import { parseJSON } from '@/utils/json';

type SetValue<T> = T | ((prevValue: T) => T);

export function useLocalStorage<T>(
  key: string,
  initialValue: T,
  options?: {
    serialize?: (value: T) => string;
    deserialize?: (value: string) => T;
  }
): [T, (value: SetValue<T>) => void, () => void] {
  const { serialize = JSON.stringify, deserialize = parseJSON } = options ?? {};
  
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? deserialize(item) : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  const setValueRef = useRef<(value: SetValue<T>) => void>();

  setValueRef.current = useCallback((value: SetValue<T>) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;

      setStoredValue(valueToStore);
      
      if (valueToStore === undefined || valueToStore === null) {
        window.localStorage.removeItem(key);
      } else {
        window.localStorage.setItem(key, serialize(valueToStore));
      }
    } catch (error) {
      console.warn(`Error setting localStorage key "${key}":`, error);
    }
  }, [key, storedValue, serialize]);

  const removeValue = useCallback(() => {
    try {
      window.localStorage.removeItem(key);
      setStoredValue(initialValue);
    } catch (error) {
      console.warn(`Error removing localStorage key "${key}":`, error);
    }
  }, [key, initialValue]);

  return [storedValue, setValueRef.current!, removeValue];
}

// 使用
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>(
    'app-theme',
    'light'
  );

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <button onClick={toggleTheme}>
      当前主题:{theme}
    </button>
  );
}

9.4 网络状态检测

typescript 复制代码
// hooks/useNetworkStatus.ts
import { useState, useEffect } from 'react';

interface NetworkStatus {
  online: boolean;
  downlink: number | null;
  effectiveType: string | null;
  rtt: number | null;
  saveData: boolean | null;
}

export function useNetworkStatus(): NetworkStatus {
  const [status, setStatus] = useState<NetworkStatus>({
    online: navigator.onLine,
    downlink: null,
    effectiveType: null,
    rtt: null,
    saveData: null
  });

  useEffect(() => {
    const connection = (navigator as any)?.connection;

    const updateOnlineStatus = () =>
      setStatus(prev => ({ ...prev, online: navigator.onLine }));

    const updateConnectionInfo = () => {
      if (!connection) return;
      
      setStatus(prev => ({
        ...prev,
        downlink: connection.downlink,
        effectiveType: connection.effectiveType,
        rtt: connection.rtt,
        saveData: connection.saveData
      }));
    };

    window.addEventListener('online', updateOnlineStatus);
    window.addEventListener('offline', updateOnlineStatus);
    
    if (connection) {
      connection.addEventListener('change', updateConnectionInfo);
      updateConnectionInfo();
    }

    return () => {
      window.removeEventListener('online', updateOnlineStatus);
      window.removeEventListener('offline', updateOnlineStatus);
      connection?.removeEventListener('change', updateConnectionInfo);
    };
  }, []);

  return status;
}

// 使用
function NetworkAwareApp() {
  const network = useNetworkStatus();

  useEffect(() => {
    if (!network.online) {
      showOfflineNotification();
    }
  }, [network.online]);

  // 根据网络质量调整加载策略
  const shouldLoadImages = 
    network.downlink !== null && network.downlink > 1.5 ||
    network.effectiveType !== '2g' && network.effectiveType !== 'slow-2g';

  return (
    <div className={network.online ? '' : 'offline-mode'}>
      {!network.online && <OfflineBanner />}
      {shouldLoadImages ? <ImageGallery /> : <TextOnlyContent />}
    </div>
  );
}

9.5 复制到剪贴板

typescript 复制代码
// hooks/useClipboard.ts
import { useState, useCallback } from 'react';

interface UseClipboardOptions {
  successDuration?: number;
}

interface UseClipboardReturn {
  copied: boolean;
  copy: (text: string) => Promise<boolean>;
  resetCopied: () => void;
}

export function useClipboard(options: UseClipboardOptions = {}): UseClipboardReturn {
  const { successDuration = 2000 } = options;
  const [copied, setCopied] = useState(false);
  const timerRef = useRef<NodeJS.Timeout>();

  const copy = useCallback(async (text: string): Promise<boolean> => {
    try {
      await navigator.clipboard.writeText(text);
      setCopied(true);
      
      timerRef.current = setTimeout(() => setCopied(false), successDuration);
      
      return true;
    } catch (err) {
      console.error('Failed to copy:', err);
      setCopied(false);
      return false;
    }
  }, [successDuration]);

  const resetCopied = useCallback(() => {
    setCopied(false);
    clearTimeout(timerRef.current);
  }, []);

  useEffect(() => {
    return () => clearTimeout(timerRef.current);
  }, []);

  return { copied, copy, resetCopied };
}

// 使用
function CopyButton({ text }: { text: string }) {
  const { copied, copy } = useClipboard();

  return (
    <button onClick={() => copy(text)}>
      {copied ? '已复制!' : '复制'}
    </button>
  );
}

十、表单处理进阶

10.1 复杂表单验证

typescript 复制代码
// hooks/useFormValidation.ts
import { useState, useCallback, useMemo } from 'react';

interface ValidationRule<T> {
  required?: boolean;
  minLength?: number;
  maxLength?: number;
  pattern?: RegExp;
  custom?: (value: T, allValues: Record<string, any>) => string | null;
  message?: string;
}

interface FormConfig<T extends Record<string, any>> {
  initialValues: T;
  validationRules: Partial<{ [K in keyof T]: ValidationRule<T[K]>[] }>;
  onSubmit: (values: T) => Promise<void>;
}

interface FormState<T> {
  values: T;
  errors: Partial<Record<keyof T, string>>;
  touched: Partial<Record<keyof T, boolean>>;
  submitting: boolean;
}

interface FormActions<T> {
  setFieldValue: (field: keyof T, value: T[keyof T]) => void;
  setFieldTouched: (field: keyof T) => void;
  handleSubmit: (e: React.FormEvent) => Promise<void>;
  resetForm: () => void;
  validateField: (field: keyof T) => string | null;
  validateAll: () => boolean;
}

export function useFormValidation<T extends Record<string, any>>(
  config: FormConfig<T>
): FormState<T> & FormActions<T> {
  const { initialValues, validationRules, onSubmit } = config;
  
  const [state, setState] = useState<FormState<T>>({
    values: { ...initialValues },
    errors: {},
    touched: {},
    submitting: false
  });

  const validateField = useCallback((field: keyof T): string | null => {
    const value = state.values[field];
    const rules = validationRules[field];
    
    if (!rules) return null;

    for (const rule of rules) {
      if (rule.required && (!value || String(value).trim() === '')) {
        return rule.message || `${String(field)} 为必填项`;
      }
      
      if (rule.minLength && String(value).length < rule.minLength) {
        return rule.message || `最少需要 ${rule.minLength} 个字符`;
      }
      
      if (rule.maxLength && String(value).length > rule.maxLength) {
        return rule.message || `最多 ${rule.maxLength} 个字符`;
      }
      
      if (rule.pattern && !rule.pattern.test(String(value))) {
        return rule.message || '格式不正确';
      }
      
      if (rule.custom) {
        const error = rule.custom(value, state.values);
        if (error) return error;
      }
    }

    return null;
  }, [state.values, validationRules]);

  const validateAll = useCallback((): boolean => {
    const newErrors: Partial<Record<keyof T, string>> = {};
    let isValid = true;

    Object.keys(validationRules).forEach(field => {
      const error = validateField(field as keyof T);
      if (error) {
        newErrors[field as keyof T] = error;
        isValid = false;
      }
    });

    setState(prev => ({ ...prev, errors: newErrors }));
    return isValid;
  }, [validateField, validationRules]);

  const setFieldValue = useCallback((field: keyof T, value: T[keyof T]) => {
    setState(prev => ({
      ...prev,
      values: { ...prev.values, [field]: value },
      errors: prev.touched[field]
        ? { ...prev.errors, [field]: validateField(field) || undefined }
        : prev.errors
    }));
  }, [validateField]);

  const setFieldTouched = useCallback((field: keyof T) => {
    setState(prev => ({
      ...prev,
      touched: { ...prev.touched, [field]: true },
      errors: { ...prev.errors, [field]: validateField(field) || undefined }
    }));
  }, [validateField]);

  const handleSubmit = useCallback(async (e: React.FormEvent) => {
    e.preventDefault();
    
    const allTouched = Object.keys(validationRules).reduce(
      (acc, key) => ({ ...acc, [key]: true }),
      {} as Partial<Record<keyof T, boolean>>
    );

    setState(prev => ({ ...prev, touched: allTouched }));

    if (!validateAll()) return;

    setState(prev => ({ ...prev, submitting: true }));

    try {
      await onSubmit(state.values);
    } finally {
      setState(prev => ({ ...prev, submitting: false }));
    }
  }, [onSubmit, state.values, validateAll, validationRules]);

  const resetForm = useCallback(() => {
    setState({
      values: { ...initialValues },
      errors: {},
      touched: {},
      submitting: false
    });
  }, [initialValues]);

  return {
    ...state,
    setFieldValue,
    setFieldTouched,
    handleSubmit,
    resetForm,
    validateField,
    validateAll
  };
}

// 使用
function RegistrationForm() {
  const form = useFormValidation({
    initialValues: {
      username: '',
      email: '',
      password: '',
      confirmPassword: '',
      agreeTerms: false
    },
    validationRules: {
      username: [
        { required: true, message: '请输入用户名' },
        { minLength: 3, message: '用户名至少3个字符' },
        { maxLength: 20, message: '用户名最多20个字符' },
        { pattern: /^[a-zA-Z0-9_]+$/, message: '只能包含字母、数字和下划线' }
      ],
      email: [
        { required: true, message: '请输入邮箱' },
        { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式不正确' }
      ],
      password: [
        { required: true, message: '请输入密码' },
        { minLength: 8, message: '密码至少8位' },
        { custom: (value, all) => {
          if (all.confirmPassword && value !== all.confirmPassword) {
            return '两次输入的密码不一致';
          }
          return null;
        }}
      ],
      confirmPassword: [
        { required: true, message: '请确认密码' },
        { custom: (value, all) => {
          if (all.password && value !== all.password) {
            return '两次输入的密码不一致';
          }
          return null;
        }}
      ],
      agreeTerms: [
        { custom: (value) => value ? null : '请同意服务条款' }
      ]
    },
    onSubmit: async (values) => {
      await api.register(values);
      toast.success('注册成功!');
    }
  });

  return (
    <form onSubmit={form.handleSubmit}>
      <FormField label="用户名" error={form.errors.username}>
        <input
          name="username"
          value={form.values.username}
          onChange={(e) => form.setFieldValue('username', e.target.value)}
          onBlur={() => form.setFieldTouched('username')}
        />
      </FormField>

      {/* 其他字段... */}
      
      <button type="submit" disabled={form.submitting}>
        {form.submitting ? '注册中...' : '注册'}
      </button>
    </form>
  );
}

10.2 表单数组操作

typescript 复制代码
// hooks/useFieldArray.ts
import { useState, useCallback } from 'react';

interface FieldArrayItem {
  id: string;
  [key: string]: any;
}

interface UseFieldArrayReturn<T extends FieldArrayItem> {
  items: T[];
  append: (item: Omit<T, 'id'>) => void;
  prepend: (item: Omit<T, 'id'>) => void;
  remove: (index: number) => void;
  insert: (index: number, item: Omit<T, 'id'>) => void;
  move: (fromIndex: number, toIndex: number) => void;
  swap: (indexA: number, indexB: number) => void;
  replace: (index: number, item: Omit<T, 'id'>) => void;
  clear: () => void;
  update: (index: number, field: string, value: any) => void;
}

let idCounter = 0;

export function useFieldArray<T extends FieldArrayItem>(
  initialItems: Omit<T, 'id'>[] = []
): UseFieldArrayReturn<T> {
  const [items, setItems] = useState<T[]>(
    initialItems.map(item => ({ ...item, id: `field-${++idCounter}` }))
  );

  const append = useCallback((item: Omit<T, 'id'>) => {
    setItems(prev => [...prev, { ...item, id: `field-${++idCounter}` }]);
  }, []);

  const prepend = useCallback((item: Omit<T, 'id'>) => {
    setItems(prev => [{ ...item, id: `field-${++idCounter}` }, ...prev]);
  }, []);

  const remove = useCallback((index: number) => {
    setItems(prev => prev.filter((_, i) => i !== index));
  }, []);

  const insert = useCallback((index: number, item: Omit<T, 'id'>) => {
    setItems(prev => [
      ...prev.slice(0, index),
      { ...item, id: `field-${++idCounter}` },
      ...prev.slice(index)
    ]);
  }, []);

  const move = useCallback((fromIndex: number, toIndex: number) => {
    setItems(prev => {
      const result = [...prev];
      const [moved] = result.splice(fromIndex, 1);
      result.splice(toIndex, 0, moved);
      return result;
    });
  }, []);

  const swap = useCallback((indexA: number, indexB: number) => {
    setItems(prev => {
      const result = [...prev];
      [result[indexA], result[indexB]] = [result[indexB], result[indexA]];
      return result;
    });
  }, []);

  const replace = useCallback((index: number, item: Omit<T, 'id'>) => {
    setItems(prev => prev.map((el, i) => i === index ? { ...item, id: el.id } : el));
  }, []);

  const clear = useCallback(() => {
    setItems([]);
  }, []);

  const update = useCallback((index: number, field: string, value: any) => {
    setItems(prev => prev.map((el, i) => i === index ? { ...el, [field]: value } : el));
  }, []);

  return { items, append, prepend, remove, insert, move, swap, replace, clear, update };
}

// 使用 - 动态表单列表
function ContactForm() {
  const contacts = useFieldArray([
    { name: '', phone: '', email: '', relation: 'friend' }
  ]);

  return (
    <form>
      <h3>联系人信息</h3>
      
      {contacts.items.map((contact, index) => (
        <div key={contact.id} className="contact-item">
          <input
            placeholder="姓名"
            value={contact.name}
            onChange={(e) => contacts.update(index, 'name', e.target.value)}
          />
          <input
            placeholder="电话"
            value={contact.phone}
            onChange={(e) => contacts.update(index, 'phone', e.target.value)}
          />
          
          <button type="button" onClick={() => contacts.remove(index)}>
            删除
          </button>
          <button
            type="button"
            onClick={() => contacts.insert(index + 1, { name: '', phone: '', email: '', relation: 'other' })}
          >
            在下方添加
          </button>
        </div>
      ))}
      
      <button type="button" onClick={() => contacts.append({ name: '', phone: '', email: '', relation: 'other' })}>
        添加联系人
      </button>
    </form>
  );
}

10.3 表单联动与依赖

typescript 复制代码
// hooks/useDependentFields.ts
import { useState, useEffect, useCallback } from 'react';

interface FieldDependency {
  source: string;
  target: string;
  transform: (sourceValue: any) => any;
  condition?: (sourceValue: any) => boolean;
}

export function useFormDependencies<T extends Record<string, any>>(
  initialValues: T,
  dependencies: FieldDependency[]
) {
  const [values, setValues] = useState<T>(initialValues);
  const [disabledFields, setDisabledFields] = useState<Set<string>>(new Set());
  const [hiddenFields, setHiddenFields] = useState<Set<string>>(new Set());

  const updateField = useCallback((field: keyof T, value: any) => {
    setValues(prev => ({ ...prev, [field]: value }));
  }, []);

  useEffect(() => {
    dependencies.forEach(dep => {
      const sourceValue = values[dep.source];
      
      if (dep.condition && !dep.condition(sourceValue)) {
        return;
      }

      const newValue = dep.transform(sourceValue);
      
      if (newValue !== values[dep.target]) {
        setValues(prev => ({ ...prev, [dep.target]: newValue }));
      }
    });
  }, [values, dependencies]);

  const setFieldDisabled = useCallback((fields: string[], disabled: boolean) => {
    setDisabledFields(prev => {
      const next = new Set(prev);
      fields.forEach(f => disabled ? next.add(f) : next.delete(f));
      return next;
    });
  }, []);

  const setFieldHidden = useCallback((fields: string[], hidden: boolean) => {
    setHiddenFields(prev => {
      const next = new Set(prev);
      fields.forEach(f => hidden ? next.add(f) : next.delete(f));
      return next;
    });
  }, []);

  return {
    values,
    updateField,
    disabledFields,
    hiddenFields,
    setFieldDisabled,
    setFieldHidden
  };
}

// 使用 - 地区级联选择器
function AddressForm() {
  const { values, updateField, disabledFields } = useFormDependencies(
    { province: '', city: '', district: '' },
    [
      {
        source: 'province',
        target: 'city',
        transform: (province) => '',
        condition: (v) => !!v
      },
      {
        source: 'city',
        target: 'district',
        transform: () => ''
      }
  ]
  );

  useEffect(() => {
    if (values.province) {
      setFieldDisabled(['city'], false);
    } else {
      setFieldDisabled(['city', 'district'], true);
    }
  }, [values.province]);

  useEffect(() => {
    if (values.city) {
      setFieldDisabled(['district'], false);
    } else {
      setFieldDisabled(['district'], true);
    }
  }, [values.city]);

  return (
    <div>
      <select
        value={values.province}
        onChange={(e) => updateField('province', e.target.value)}
      >
        <option value="">选择省份</option>
        {provinces.map(p => <option key={p.code} value={p.code}>{p.name}</option>)}
      </select>

      <select
        value={values.city}
        onChange={(e) => updateField('city', e.target.value)}
        disabled={disabledFields.has('city')}
      >
        <option value="">选择城市</option>
        {getCities(values.province).map(c => <option key={c.code} value={c.code}>{c.name}</option>)}
      </select>

      <select
        value={values.district}
        onChange={(e) => updateField('district', e.target.value)}
        disabled={disabledFields.has('district')}
      >
        <option value="">选择区县</option>
        {getDistricts(values.city).map(d => <option key={d.code} value={d.code}>{d.name}</option>)}
      </select>
    </div>
  );
}

十一、动画与交互

11.1 CSS 动画 Hook

typescript 复制代码
// hooks/useAnimation.ts
import { useState, useRef, useEffect, useCallback } from 'react';

interface AnimationConfig {
  duration?: number;
  easing?: string;
  delay?: number;
  iterations?: number | 'infinite';
}

interface UseAnimationReturn {
  ref: React.RefObject<HTMLElement>;
  animate: (keyframes: Keyframe[], config?: AnimationConfig) => Animation;
  pause: () => void;
  resume: () => void;
  cancel: () => void;
  isAnimating: boolean;
}

export function useAnimation(): UseAnimationReturn {
  const ref = useRef<HTMLElement>(null);
  const [animation, setAnimation] = useState<Animation | null>(null);

  const animate = useCallback(
    (keyframes: Keyframe[], config: AnimationConfig = {}): Animation => {
      const element = ref.current;
      if (!element) throw new Error('Element not found');

      const {
        duration = 300,
        easing = 'ease',
        delay = 0,
        iterations = 1
      } = config;

      const anim = element.animate(keyframes, { duration, easing, delay, iterations });
      
      setAnimation(anim);
      
      anim.onfinish = () => setAnimation(null);

      return anim;
    },
    []
  );

  const pause = useCallback(() => animation?.pause(), [animation]);
  const resume = useCallback(() => animation?.play(), [animation]);
  const cancel = useCallback(() => {
    animation?.cancel();
    setAnimation(null);
  }, [animation]);

  return {
    ref,
    animate,
    pause,
    resume,
    cancel,
    isAnimating: !!animation
  };
}

// 使用 - 淡入淡出
function FadeInOut({ show, children }: { show: boolean; children: React.ReactNode }) {
  const { ref, isAnimating } = useAnimation();

  useEffect(() => {
    if (!ref.current) return;

    if (show) {
      ref.current.animate(
        [
          { opacity: 0, transform: 'translateY(-10px)' },
          { opacity: 1, transform: 'translateY(0)' }
        ],
        { duration: 200, easing: 'ease-out' }
      );
    }
  }, [show]);

  return (
    <div
      ref={ref}
      style={{ display: show ? 'block' : 'none', pointerEvents: isAnimating ? 'none' : 'auto' }}
    >
      {children}
    </div>
  );
}

// 使用 - 弹跳效果
function BounceButton() {
  const { ref, animate } = useAnimation();

  const handleClick = () => {
    animate(
      [
        { transform: 'scale(1)', offset: 0 },
        { transform: 'scale(0.95)', offset: 0.5 },
        { transform: 'scale(1.05)', offset: 0.7 },
        { transform: 'scale(1)', offset: 1 }
      ],
      { duration: 300, easing: 'ease-in-out' }
    );
  };

  return <button ref={ref} onClick={handleClick}>点击弹跳</button>;
}

11.2 过渡动画组件

tsx 复制代码
// components/Transition.tsx
import React, { useState, useEffect, useRef, ReactNode } from 'react';

type TransitionState = 'entering' | 'entered' | 'exiting' | 'exited';

interface TransitionProps {
  in: boolean;
  timeout?: number;
  children: ReactNode;
  onEnter?: () => void;
  onEntered?: () => void;
  onExit?: () => void;
  onExited?: () => void;
  unmountOnExit?: boolean;
}

export function Transition({
  in: show,
  timeout = 300,
  children,
  onEnter,
  onEntered,
  onExit,
  onExited,
  unmountOnExit = false
}: TransitionProps) {
  const [state, setState] = useState<TransitionState>(show ? 'entered' : 'exited');
  const shouldRender = !unmountOnExit || state !== 'exited';
  const timerRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    if (show) {
      setState('entering');
      onEnter?.();
      
      timerRef.current = setTimeout(() => {
        setState('entered');
        onEntered?.();
      }, 10); // 短暂延迟确保 DOM 更新
    } else if (state !== 'exited') {
      setState('exiting');
      onExit?.();
      
      timerRef.current = setTimeout(() => {
        setState('exited');
        onExited?.();
      }, timeout);
    }

    return () => clearTimeout(timerRef.current);
  }, [show]);

  if (!shouldRender) return null;

  return <div className={`transition-${state}`}>{children}</div>;
}

// 使用
function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
  return (
    <Transition in={isOpen} timeout={300}>
      <div className="modal-overlay" onClick={onClose}>
        <Transition in={isOpen} timeout={200}>
          <div className="modal-content" onClick={(e) => e.stopPropagation()}>
            <h2>模态框</h2>
            <p>内容</p>
            <button onClick={onClose}>关闭</button>
          </div>
        </Transition>
      </div>
    </Transition>
  );
}

// CSS 配合
// .transition-entering { opacity: 0; transform: scale(0.9); }
// .transition-entered { opacity: 1; transform: scale(1); transition: all 0.3s ease; }
// .transition-exiting { opacity: 1; transform: scale(1); }
// .transition-exited { opacity: 0; transform: scale(0.9); transition: all 0.3s ease; }

11.3 拖拽交互

typescript 复制代码
// hooks/useDrag.ts
import { useState, useCallback, useRef, useEffect } from 'react';

interface Position {
  x: number;
  y: number;
}

interface UseDragOptions {
  bounds?: HTMLElement | string;
  onStart?: (position: Position) => void;
  onDrag?: (position: Position) => void;
  onEnd?: (position: Position) => void;
  disabled?: boolean;
}

interface UseDragReturn {
  ref: React.RefObject<HTMLElement>;
  position: Position;
  isDragging: boolean;
}

export function useDrag(options: UseDragOptions = {}): UseDragReturn {
  const { bounds, onStart, onDrag, onEnd, disabled = false } = options;
  
  const ref = useRef<HTMLElement>(null);
  const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
  const [isDragging, setIsDragging] = useState(false);
  const dragStartRef = useRef<Position>({ x: 0, y: 0 });

  const getBoundsRect = useCallback(() => {
    if (!bounds) return null;
    
    if (typeof bounds === 'string') {
      return document.querySelector(bounds)?.getBoundingClientRect() ?? null;
    }
    
    return bounds.getBoundingClientRect();
  }, [bounds]);

  const handleMouseDown = useCallback((e: MouseEvent) => {
    if (disabled) return;

    e.preventDefault();
    setIsDragging(true);
    dragStartRef.current = { x: e.clientX - position.x, y: e.clientY - position.y };
    onStart?.(position);
  }, [disabled, position, onStart]);

  const handleMouseMove = useCallback((e: MouseEvent) => {
    if (!isDragging || disabled) return;

    let newX = e.clientX - dragStartRef.current.x;
    let newY = e.clientY - dragStartRef.current.y;

    // 边界约束
    const boundsRect = getBoundsRect();
    const element = ref.current;
    
    if (boundsRect && element) {
      const elWidth = element.offsetWidth;
      const elHeight = element.offsetHeight;

      newX = Math.max(boundsRect.left, Math.min(newX, boundsRect.right - elWidth));
      newY = Math.max(boundsRect.top, Math.min(newY, boundsRect.bottom - elHeight));
    }

    setPosition({ x: newX, y: newY });
    onDrag?.({ x: newX, y: newY });
  }, [isDragging, disabled, getBoundsRect, onDrag]);

  const handleMouseUp = useCallback(() => {
    setIsDragging(false);
    onEnd?.(position);
  }, [onEnd, position]);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    element.addEventListener('mousedown', handleMouseDown);
    
    return () => element.removeEventListener('mousedown', handleMouseDown);
  }, [handleMouseDown]);

  useEffect(() => {
    if (!isDragging) return;

    window.addEventListener('mousemove', handleMouseMove);
    window.addEventListener('mouseup', handleMouseUp);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isDragging, handleMouseMove, handleMouseUp]);

  return { ref, position, isDragging };
}

// 使用 - 可拖拽面板
function DraggablePanel() {
  const { ref, position, isDragging } = useDrag({
    bounds: '.app-container',
    onEnd: (pos) => console.log('最终位置:', pos)
  });

  return (
    <div
      ref={ref}
      style={{
        position: 'absolute',
        left: position.x,
        top: position.y,
        cursor: isDragging ? 'grabbing' : 'grab',
        userSelect: isDragging ? 'none' : 'auto'
      }}
    >
      <div className="panel-header">拖拽我</div>
      <div className="panel-body">内容区域</div>
    </div>
  );
}

11.4 触摸手势

typescript 复制代码
// hooks/useGestures.ts
import { useRef, useCallback, useEffect } from 'react';

interface GestureHandlers {
  onTap?: (point: { x: number; y: number }) => void;
  onDoubleTap?: (point: { x: number; y: number }) => void;
  onSwipeLeft?: () => void;
  onSwipeRight?: () => void;
  onSwipeUp?: () => void;
  onSwipeDown?: () => void;
  onPinchStart?: (distance: number) => void;
  onPinchMove?: (scale: number) => void;
  onPinchEnd?: (scale: number) => void;
}

interface UseGesturesOptions {
  threshold?: number;
  swipeThreshold?: number;
  doubleTapDelay?: number;
}

export function useGestures(
  handlers: GestureHandlers,
  options: UseGesturesOptions = {}
): React.RefObject<HTMLElement> {
  const {
    threshold = 10,
    swipeThreshold = 50,
    doubleTapDelay = 300
  } = options;

  const ref = useRef<HTMLElement>(null);
  const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null);
  const lastTapRef = useRef<number>(0);
  const initialPinchDistance = useRef<number>(0);

  const handleTouchStart = useCallback((e: TouchEvent) => {
    const touch = e.touches[0];
    touchStartRef.current = {
      x: touch.clientX,
      y: touch.clientY,
      time: Date.now()
    };

    // 双击检测
    if (Date.now() - lastTapRef.current < doubleTapDelay) {
      handlers.onDoubleTap?.({ x: touch.clientX, y: touch.clientY });
    }
    lastTapRef.current = Date.now();

    // 捏合手势开始
    if (e.touches.length === 2) {
      initialPinchDistance.current = getPinchDistance(e.touches[0], e.touches[1]);
      handlers.onPinchStart?.(initialPinchDistance.current);
    }
  }, [handlers, doubleTapDelay]);

  const handleTouchEnd = useCallback((e: TouchEvent) => {
    if (!touchStartRef.current) return;

    const touch = e.changedTouches[0];
    const deltaX = touch.clientX - touchStartRef.current.x;
    const deltaY = touch.clientY - touchStartRef.current.y;
    const deltaTime = Date.now() - touchStartRef.current.time;

    // 点击检测(移动距离小且时间短)
    if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold && deltaTime < 300) {
      handlers.onTap?.({ x: touch.clientX, y: touch.clientY });
    }

    // 滑动检测
    if (Math.abs(deltaX) > swipeThreshold && Math.abs(deltaX) > Math.abs(deltaY)) {
      if (deltaX > 0) handlers.onSwipeRight?.();
      else handlers.onSwipeLeft?.();
    }

    if (Math.abs(deltaY) > swipeThreshold && Math.abs(deltaY) > Math.abs(deltaX)) {
      if (deltaY > 0) handlers.onSwipeDown?.();
      else handlers.onSwipeUp?.();
    }

    // 捏合结束
    if (e.touches.length === 0 && initialPinchDistance.current > 0) {
      handlers.onPinchEnd?.(initialPinchDistance.current);
      initialPinchDistance.current = 0;
    }

    touchStartRef.current = null;
  }, [handlers, threshold, swipeThreshold]);

  const handleTouchMove = useCallback((e: TouchEvent) => {
    if (e.touches.length === 2 && initialPinchDistance.current > 0) {
      const currentDistance = getPinchDistance(e.touches[0], e.touches[1]);
      const scale = currentDistance / initialPinchDistance.current;
      handlers.onPinchMove?.(scale);
    }
  }, [handlers]);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    element.addEventListener('touchstart', handleTouchStart as EventListener);
    element.addEventListener('touchend', handleTouchEnd as EventListener);
    element.addEventListener('touchmove', handleTouchMove as EventListener);

    return () => {
      element.removeEventListener('touchstart', handleTouchStart as EventListener);
      element.removeEventListener('touchend', handleTouchEnd as EventListener);
      element.removeEventListener('touchmove', handleTouchMove as EventListener);
    };
  }, [handleTouchStart, handleTouchEnd, handleTouchMove]);

  return ref;
}

function getPinchDistance(touch1: Touch, touch2: Touch): number {
  const dx = touch1.clientX - touch2.clientX;
  const dy = touch1.clientY - touch2.clientY;
  return Math.sqrt(dx * dx + dy * dy);
}

// 使用 - 图片查看器
function ImageViewer({ src }: { src: string }) {
  const [scale, setScale] = useState(1);
  const imageRef = useGestures({
    onDoubleTap: () => setScale(prev => prev === 1 ? 2 : 1),
    onSwipeLeft: () => nextImage(),
    onSwipeRight: () => prevImage(),
    onPinchMove: (s) => setScale(Math.max(0.5, Math.min(s, 4)))
  });

  return (
    <div ref={imageRef} className="image-viewer">
      <img
        src={src}
        alt=""
        style={{ transform: `scale(${scale})`, transition: 'transform 0.1s' }}
      />
    </div>
  );
}

十二、数据获取与缓存策略

12.1 React Query 深度使用

typescript 复制代码
// hooks/useQueryWithCache.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from '@/utils/toast';

interface UseQueryOptions<T> {
  queryKey: unknown[];
  queryFn: () => Promise<T>;
  staleTime?: number;
  cacheTime?: number;
  retry?: number | ((failureCount: number, error: Error) => boolean);
  onSuccess?: (data: T) => void;
  onError?: (error: Error) => void;
}

export function useDataQuery<T>({
  queryKey,
  queryFn,
  staleTime = 5 * 60 * 1000, // 5分钟
  cacheTime = 30 * 60 * 1000, // 30分钟
  retry = 2,
  onSuccess,
  onError
}: UseQueryOptions<T>) {
  return useQuery({
    queryKey,
    queryFn,
    staleTime,
    cacheTime: cacheTime,
    retry,
    onSuccess,
    onError: (err) => {
      onError?.(err as Error);
      toast.error('数据加载失败');
    },
    select: (data) => data
  });
}

// 分页查询
function usePaginatedList(page: number, pageSize: number, filters?: Record<string, any>) {
  return useQuery({
    queryKey: ['list', page, pageSize, filters],
    queryFn: () => api.getList(page, pageSize, filters),
    keepPreviousData: true, // 切换页时保留旧数据,避免闪烁
    staleTime: 30 * 1000
  });
}

// 无限查询
function useInfinitePosts() {
  return useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam = 1 }) => api.getPosts(pageParam),
    getNextPageParam: (lastPage, allPages) =>
      lastPage.hasMore ? allPages.length + 1 : undefined,
    staleTime: 60 * 1000
  });
}

// 使用
function PostFeed() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage
  } = useInfinitePosts();

  return (
    <div>
      {data?.pages.map((page, i) => (
        <React.Fragment key={i}>
          {page.items.map(post => <PostCard key={post.id} post={post} />)}
        </React.Fragment>
      ))}
      
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? '加载中...' : '加载更多'}
        </button>
      )}
    </div>
  );
}

12.2 乐观更新

typescript 复制代码
// hooks/useOptimisticMutation.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { produce } from 'immer';

interface OptimisticUpdateOptions<T> {
  mutationFn: (data: T) => Promise<any>;
  queryKey: string[];
  updateFn: (oldData: T[] | undefined, newData: T) => T[];
  rollbackMessage?: string;
}

export function useOptimisticMutation<T extends { id: string | number }>({
  mutationFn,
  queryKey,
  updateFn,
  rollbackMessage = '操作失败,已回滚'
}: OptimisticUpdateOptions<T>) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn,
    onMutate: async (newItem) => {
      await queryClient.cancelQueries(queryKey);

      const previousData = queryClient.getQueryData<T[]>(queryKey);

      queryClient.setQueryData<T[]>(queryKey, old =>
        updateFn(old, newItem)
      );

      return { previousData };
    },
    onError: (_err, _newItem, context) => {
      queryClient.setQueryData(queryKey, context.previousData);
      toast.error(rollbackMessage);
    },
    onSettled: () => {
      queryClient.invalidateQueries(queryKey);
    }
  });
}

// 使用 - 点赞功能
function LikeButton({ postId }: { postId: string }) {
  const queryClient = useQueryClient();
  
  const likeMutation = useOptimisticMutation<Post>({
    mutationFn: (post) => api.toggleLike(post.id),
    queryKey: ['posts'],
    updateFn: (posts, updatedPost) => 
      produce(posts ?? [], draft => {
        const index = draft.findIndex(p => p.id === updatedPost.id);
        if (index !== -1) {
          draft[index].liked = !draft[index].liked;
          draft[index].likeCount += draft[index].liked ? 1 : -1;
        }
      })
  });

  const post = useGetPost(postId);

  return (
    <button
      onClick={() => likeMutation.mutate(post.data!)}
      disabled={likeMutation.isLoading}
      className={post.data?.liked ? 'liked' : ''}
    >
      ❤ {post.data?.likeCount}
    </button>
  );
}

// 使用 - 添加评论
function CommentForm({ postId }: { postId: string }) {
  const addComment = useOptimisticMutation<Comment>({
    mutationFn: (comment) => api.addComment(comment),
    queryKey: ['comments', postId],
    updateFn: (comments, newComment) => [...(comments ?? []), newComment]
  });

  const handleSubmit = (content: string) => {
    addComment.mutate({
      id: `temp-${Date.now()}`,
      content,
      author: currentUser.name,
      createdAt: new Date().toISOString(),
      pending: true
    });
  };

  return <CommentInput onSubmit={handleSubmit} />;
}

12.3 缓存失效策略

typescript 复制代码
// utils/cacheManager.ts
import { useQueryClient } from '@tanstack/react-query';

class CacheManager {
  private queryClient: ReturnType<typeof useQueryClient>;

  constructor(queryClient: ReturnType<typeof useQueryClient>) {
    this.queryClient = queryClient;
  }

  // 精确失效单个查询
  invalidateExact(key: string[]) {
    this.queryClient.invalidateQueries(key);
  }

  // 模糊匹配失效
  invalidatePattern(pattern: string) {
    this.queryClient.invalidateQueries({
      predicate: (query) => {
        const keyString = JSON.stringify(query.queryKey);
        return keyString.includes(pattern);
      }
    });
  }

  // 预取数据
  async prefetch<T>(
    key: string[],
    fetcher: () => Promise<T>,
    options?: { staleTime?: number }
  ) {
    await this.queryClient.prefetchQuery({
      queryKey: key,
      queryFn: fetcher,
      staleTime: options?.staleTime ?? 5 * 60 * 1000
    });
  }

  // 重置特定命名空间
  resetNamespace(namespace: string) {
    this.queryClient.removeQueries({
      predicate: (query) => {
        const firstKey = query.queryKey[0];
        return typeof firstKey === 'string' && firstKey.startsWith(namespace);
      }
    });
  }

  // 获取或设置默认数据
  getOrSetDefault<T>(key: string[], defaultValue: T): T {
    const cached = this.queryClient.getQueryData<T>(key);
    if (cached !== undefined) return cached;
    
    this.queryClient.setQueryData(key, defaultValue);
    return defaultValue;
  }
}

// 使用
function DataProvider({ children }: { children: React.ReactNode }) {
  const queryClient = useQueryClient();
  const cacheManager = useMemo(() => new CacheManager(queryClient), [queryClient]);

  // 在 Context 中暴露
  return (
    <CacheContext.Provider value={cacheManager}>
      {children}
    </CacheContext.Provider>
  );
}

// 组件中使用
function UserActions() {
  const cacheManager = useContext(CacheContext);

  const handleUserUpdate = async (userId: string, userData: Partial<User>) => {
    await api.updateUser(userId, userData);
    
    // 失效用户相关缓存
    cacheManager.invalidateExact(['user', userId]);
    cacheManager.invalidatePattern('user-list');
  };

  const handleLogout = () => {
    cacheManager.resetNamespace('user');
    cacheManager.resetNamespace('auth');
  };
}

十三、组件设计模式

13.1 复合组件模式

tsx 复制代码
// components/Tabs/index.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface TabsContextValue {
  activeTab: string;
  setActiveTab: (id: string) => void;
}

const TabsContext = createContext<TabsContextValue | null>(null);

function useTabsContext() {
  const context = useContext(TabsContext);
  if (!context) throw new Error('Tabs components must be used within <Tabs>');
  return context;
}

// 主容器
interface TabsProps {
  defaultActiveTab?: string;
  onChange?: (activeTab: string) => void;
  children: ReactNode;
}

export function Tabs({ defaultActiveTab, onChange, children }: TabsProps) {
  const [activeTab, setActiveTab] = useState(defaultActiveTab ?? '');

  const handleSetActiveTab = (id: string) => {
    setActiveTab(id);
    onChange?.(id);
  };

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab: handleSetActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

// 标签列表
export function TabList({ children, ...props }: { children: ReactNode }) {
  return <div role="tablist" className="tab-list" {...props}>{children}</div>;
}

// 单个标签
interface TabProps {
  id: string;
  disabled?: boolean;
  children: ReactNode;
}

export function Tab({ id, disabled, children, ...props }: TabProps) {
  const { activeTab, setActiveTab } = useTabsContext();
  const isActive = activeTab === id;

  return (
    <button
      role="tab"
      aria-selected={isActive}
      disabled={disabled}
      className={`tab ${isActive ? 'active' : ''}`}
      onClick={() => !disabled && setActiveTab(id)}
      {...props}
    >
      {children}
    </button>
  );
}

// 内容面板
interface TabPanelProps {
  id: string;
  children: ReactNode;
}

export function TabPanel({ id, children, ...props }: TabPanelProps) {
  const { activeTab } = useTabsContext();

  if (activeTab !== id) return null;

  return (
    <div role="tabpanel" id={`panel-${id}`} className="tab-panel" {...props}>
      {children}
    </div>
  );
}

// 使用
function App() {
  return (
    <Tabs defaultActiveTab="profile" onChange={(id) => console.log('切换到:', id)}>
      <TabList>
        <Tab id="profile">个人资料</Tab>
        <Tab id="settings">设置</Tab>
        <Tab id="notifications" disabled>通知</Tab>
      </TabList>

      <TabPanel id="profile">
        <ProfileForm />
      </TabPanel>
      <TabPanel id="settings">
        <SettingsForm />
      </TabPanel>
      <TabPanel id="notifications">
        <Notifications />
      </TabPanel>
    </Tabs>
  );
}

13.2 高阶组件(HOC)

typescript 复制代码
// HOC/withAuth.tsx
import React, { ComponentType, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '@/stores/auth';

interface WithAuthOptions {
  requiredPermissions?: string[];
  redirectTo?: string;
  fallback?: ComponentType;
}

export function withAuth<P extends object>(
  WrappedComponent: ComponentType<P>,
  options: WithAuthOptions = {}
) {
  const { requiredPermissions = [], redirectTo = '/login', fallback: FallbackComponent } = options;

  return function AuthenticatedComponent(props: P) {
    const navigate = useNavigate();
    const { isAuthenticated, hasPermissions } = useAuthStore();

    useEffect(() => {
      if (!isAuthenticated) {
        navigate(redirectTo, { replace: true });
        return;
      }

      if (requiredPermissions.length > 0 && !hasPermissions(requiredPermissions)) {
        navigate('/403', { replace: true });
      }
    }, [isAuthenticated, requiredPermissions]);

    if (!isAuthenticated) {
      return FallbackComponent ? <FallbackComponent /> : null;
    }

    if (requiredPermissions.length > 0 && !hasPermissions(requiredPermissions)) {
      return FallbackComponent ? <FallbackComponent /> : null;
    }

    return <WrappedComponent {...props} />;
  };
}

// HOC/withLoading.tsx
import React, { ComponentType, useState, useEffect } from 'react';

interface WithLoadingProps {
  isLoading: boolean;
  skeletonCount?: number;
}

export function withLoading<P extends object>(
  WrappedComponent: ComponentType<P>,
  SkeletonComponent: ComponentType<{ count: number }>
) {
  return function LoadingWrapper(props: P & WithLoadingProps) {
    const { isLoading, skeletonCount = 3, ...rest } = props;

    if (isLoading) {
      return <SkeletonComponent count={skeletonCount} />;
    }

    return <WrappedComponent {...(rest as P)} />;
  };
}

// HOC/withErrorBoundary.tsx
import React, { ComponentType, Component, ErrorInfo, ReactNode } from 'react';

interface ErrorBoundaryProps {
  fallback?: ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
  children: ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  state: ErrorBoundaryState = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    this.props.onError?.(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div className="error-fallback">
          <h2>出错了</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            重试
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export function withErrorBoundary<P extends object>(
  WrappedComponent: ComponentType<P>,
  errorFallback?: ReactNode
) {
  return function ErrorBoundedComponent(props: P) {
    return (
      <ErrorBoundary fallback={errorFallback}>
        <WrappedComponent {...props} />
      </ErrorBoundary>
    );
  };
}

// 组合使用
const AdminDashboard = compose(
  withAuth({ requiredPermissions: ['admin:view'] }),
  withErrorBoundary(<AdminErrorFallback />),
  withLoading(AdminSkeleton)
)(DashboardComponent);

// 使用
function App() {
  return (
    <Routes>
      <Route path="/admin" element={<AdminDashboard />} />
    </Routes>
  );
}

13.3 Render Props 变体

tsx 复制代码
// components/DataFetcher.tsx
import React, { useState, useEffect, ReactNode } from 'react';

interface DataFetcherProps<T> {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  body?: any;
  headers?: Record<string, string>;
  children: (result: {
    data: T | null;
    loading: boolean;
    error: Error | null;
    refetch: () => void;
  }) => ReactNode;
}

export function DataFetcher<T>({
  url,
  method = 'GET',
  body,
  headers,
  children
}: DataFetcherProps<T>) {
  const [state, setState] = useState<{
    data: T | null;
    loading: boolean;
    error: Error | null;
  }>({
    data: null,
    loading: true,
    error: null
  });

  const fetchData = async () => {
    setState(prev => ({ ...prev, loading: true, error: null }));

    try {
      const response = await fetch(url, {
        method,
        headers: { 'Content-Type': 'application/json', ...headers },
        body: body ? JSON.stringify(body) : undefined
      });

      if (!response.ok) throw new Error(`HTTP ${response.status}`);

      const data = await response.json();
      setState({ data, loading: false, error: null });
    } catch (err) {
      setState({ data: null, loading: false, error: err as Error });
    }
  };

  useEffect(() => {
    fetchData();
  }, [url]);

  return <>{children({ ...state, refetch: fetchData })}</>;
}

// 使用
function UserProfile({ userId }: { userId: string }) {
  return (
    <DataFetcher<User> url={`/api/users/${userId}`}>
      {({ data: user, loading, error, refetch }) => {
        if (loading) return <UserSkeleton />;
        if (error) return <ErrorMessage error={error} onRetry={refetch} />;
        
        return (
          <div className="user-profile">
            <Avatar src={user.avatarUrl} />
            <h1>{user.displayName}</h1>
            <p>{user.bio}</p>
            <button onClick={refetch}>刷新</button>
          </div>
        );
      }}
    </DataFetcher>
  );
}

十四、安全与权限控制

14.1 XSS 防护

typescript 复制代码
// utils/sanitizer.ts
import DOMPurify from 'dompurify';

interface SanitizeOptions {
  ALLOWED_TAGS?: string[];
  ALLOWED_ATTR?: string[];
  FORBID_TAGS?: string[];
  FORBID_ATTR?: string[];
}

const DEFAULT_CONFIG: SanitizeOptions = {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
  ALLOWED_ATTR: ['href', 'target', 'rel']
};

export function sanitizeHTML(dirtyHTML: string, options?: SanitizeOptions): string {
  const config = { ...DEFAULT_CONFIG, ...options };
  
  return DOMPurify.sanitize(dirtyHTML, {
    ALLOWED_TAGS: config.ALLOWED_TAGS,
    ALLOWED_ATTR: config.ALLOWED_ATTR,
    FORBID_TAGS: config.FORBID_TAGS,
    FORBID_ATTR: config.FORBID_ATTR
  });
}

// 安全渲染富文本
interface SafeHTMLProps {
  html: string;
  tag?: keyof JSX.IntrinsicElements;
  className?: string;
}

export function SafeHTML({ html, tag: Tag = 'div', className }: SafeHTMLProps) {
  const sanitized = useMemo(() => sanitizeHTML(html), [html]);

  return (
    <Tag
      className={className}
      dangerouslySetInnerHTML={{ __html: sanitized }}
    />
  );
}

// URL 安全处理
export function safeUrl(url: string): string {
  try {
    const parsed = new URL(url);
    
    // 只允许 http 和 https 协议
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return '#unsafe-url';
    }
    
    // 防止 JavaScript 协议变体
    if (/^\s*javascript:/i.test(url)) {
      return '#javascript-url';
    }
    
    return url;
  } catch {
    return '#invalid-url';
  }
}

// 安全链接组件
interface SecureLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
  href: string;
  children: ReactNode;
}

export function SecureLink({ href, children, ...props }: SecureLinkProps) {
  const safeHref = useMemo(() => safeUrl(href), [href]);

  return (
    <a
      href={safeHref}
      target="_blank"
      rel="noopener noreferrer"
      {...props}
    >
      {children}
    </a>
  );
}

// 使用
function ArticleContent({ article }: { article: Article }) {
  return (
    <article>
      <h1>{article.title}</h1>
      <SafeHTML html={article.content} />
      
      <div className="references">
        {article.references.map(ref => (
          <SecureLink key={ref.id} href={ref.url}>
            {ref.title}
          </SecureLink>
        ))}
      </div>
    </article>
  );
}

14.2 RBAC 权限系统

typescript 复制代码
// types/permissions.ts
type Permission =
  | 'user:create'
  | 'user:read'
  | 'user:update'
  | 'user:delete'
  | 'role:manage'
  | 'system:config';

type Role = 'admin' | 'manager' | 'editor' | 'viewer';

const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
  admin: Object.values({}) as Permission[],
  manager: ['user:create', 'user:read', 'user:update', 'role:manage'],
  editor: ['user:read', 'user:create'],
  viewer: ['user:read']
};

// hooks/usePermission.ts
import { useMemo } from 'react';
import { useAuthStore } from '@/stores/auth';

interface UsePermissionReturn {
  hasPermission: (permission: Permission) => boolean;
  hasAnyPermission: (permissions: Permission[]) => boolean;
  hasAllPermissions: (permissions: Permission[]) => boolean;
  role: Role | null;
}

export function usePermission(): UsePermissionReturn {
  const { user } = useAuthStore();

  const role = user?.role ?? null;
  const permissions = useMemo(() => {
    if (!role) return [];
    return ROLE_PERMISSIONS[role] ?? [];
  }, [role]);

  const hasPermission = (permission: Permission): boolean => {
    return permissions.includes(permission);
  };

  const hasAnyPermission = (perms: Permission[]): boolean => {
    return perms.some(p => permissions.includes(p));
  };

  const hasAllPermissions = (perms: Permission[]): boolean => {
    return perms.every(p => permissions.includes(p));
  };

  return { hasPermission, hasAnyPermission, hasAllPermissions, role };
}

// 权限按钮组件
interface PermissionButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  permission: Permission | Permission[];
  fallback?: ReactNode;
  children: ReactNode;
}

export function PermissionButton({
  permission,
  fallback = null,
  children,
  ...props
}: PermissionButtonProps) {
  const { hasPermission, hasAnyPermission } = usePermission();

  const permitted = Array.isArray(permission)
    ? hasAnyPermission(permission)
    : hasPermission(permission);

  if (!permitted) return <>{fallback}</>;

  return <button {...props}>{children}</button>;
}

// 路由权限守卫
interface ProtectedRouteProps {
  permission: Permission | Permission[];
  redirectTo?: string;
  children: ReactNode;
}

export function ProtectedRoute({
  permission,
  redirectTo = '/403',
  children
}: ProtectedRouteProps) {
  const { hasAnyPermission } = usePermission();
  const navigate = useNavigate();

  const permitted = Array.isArray(permission)
    ? hasAnyPermission(permission)
    : hasAnyPermission([permission]);

  useEffect(() => {
    if (!permitted) {
      navigate(redirectTo, { replace: true });
    }
  }, [permitted, redirectTo, navigate]);

  if (!permitted) return null;

  return <>{children}</>;
}

// 使用
function UserManagement() {
  const { hasPermission, role } = usePermission();

  return (
    <div>
      <h1>用户管理</h1>
      
      {/* 基础权限检查 */}
      {hasPermission('user:create') && (
        <Button variant="primary">创建用户</Button>
      )}

      {/* 多权限检查 */}
      {(hasPermission('user:update') || hasPermission('user:delete')) && (
        <Table actions={['edit', 'delete']} />
      )}

      {/* 使用权限按钮 */}
      <PermissionButton permission="user:delete">
        <Button variant="danger">删除</Button>
      </PermissionButton>

      <PermissionButton permission={['user:update', 'user:delete']}>
        <Button variant="primary">批量操作</Button>
      </PermissionButton>

      {/* 角色显示 */}
      <Badge variant={role}>当前角色:{role}</Badge>
    </div>
  );
}

14.3 CSRF 防护

typescript 复制代码
// utils/csrf.ts
import axios from 'axios';

let csrfToken: string | null = null;

export async function fetchCsrfToken(): Promise<string> {
  try {
    const response = await axios.get('/api/csrf-token');
    csrfToken = response.data.token;
    return csrfToken;
  } catch (error) {
    console.error('Failed to fetch CSRF token:', error);
    throw error;
  }
}

export function getCsrfToken(): string | null {
  return csrfToken;
}

// Axios 拦截器自动添加 CSRF Token
export function setupCsrfInterceptor(instance: axios.AxiosInstance) {
  instance.interceptors.request.use(async (config) => {
    // GET 请求不需要 CSRF
    if (config.method?.toLowerCase() === 'get') {
      return config;
    }

    // 获取或刷新 token
    let token = csrfToken;
    if (!token) {
      token = await fetchCsrfToken();
    }

    // 添加到请求头
    config.headers['X-CSRF-Token'] = token;

    return config;
  });

  instance.interceptors.response.use(
    (response) => response,
    async (error) => {
      // 如果是 CSRF 错误,尝试重新获取 token 并重试
      if (error.response?.status === 403 && error.response?.data?.code === 'CSRF_INVALID') {
        await fetchCsrfToken();
        
        // 重试原始请求
        const originalRequest = error.config;
        originalRequest.headers['X-CSRF-Token'] = csrfToken!;
        
        return instance(originalRequest);
      }

      return Promise.reject(error);
    }
  );
}

// 表单提交包装器
interface FormSubmitOptions {
  url: string;
  data: FormData | Record<string, any>;
  method?: 'POST' | 'PUT' | 'DELETE';
}

export async function secureFormSubmit({ url, data, method = 'POST' }: FormSubmitOptions) {
  const token = await fetchCsrfToken();

  if (data instanceof FormData) {
    data.append('_csrf', token);
  } else {
    data._csrf = token;
  }

  return axios({
    method,
    url,
    data,
    headers: {
      'Content-Type': data instanceof FormData ? undefined : 'application/json',
      'X-CSRF-Token': token
    }
  });
}

// 使用
async function handleDeleteUser(userId: string) {
  try {
    await secureFormSubmit({
      url: `/api/users/${userId}`,
      method: 'DELETE',
      data: {}
    });
    toast.success('删除成功');
  } catch (error) {
    toast.error('操作失败');
  }
}

十五、测试策略

15.1 单元测试最佳实践

typescript 复制代码
// __tests__/hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react-hooks';
import { describe, it, expect, beforeEach } from 'vitest';
import { useCounter } from '@/hooks/useCounter';

describe('useCounter', () => {
  it('应该返回初始值', () => {
    const { result } = renderHook(() => useCounter());
    
    expect(result.current.count).toBe(0);
  });

  it('应该支持自定义初始值', () => {
    const { result } = renderHook(() => useCounter(10));
    
    expect(result.current.count).toBe(10);
  });

  it('increment 应该增加计数', () => {
    const { result } = renderHook(() => useCounter());
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });

  it('decrement 应该减少计数', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.decrement();
    });
    
    expect(result.current.count).toBe(4);
  });

  it('reset 应该重置为初始值', () => {
    const { result } = renderHook(() => useCounter(10));
    
    act(() => {
      result.current.increment();
      result.current.increment();
      result.current.reset();
    });
    
    expect(result.current.count).toBe(10);
  });

  it('支持步长参数', () => {
    const { result } = renderHook(() => useCounter(0, 5));
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(5);
  });
});

// __tests__/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import Button from '@/components/Button';

describe('Button', () => {
  it('应该正确渲染文本', () => {
    render(<Button>点击我</Button>);
    
    expect(screen.getByText('点击我')).toBeInTheDocument();
  });

  it('点击时应该调用 onClick', () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>点击</Button>);
    
    fireEvent.click(screen.getByRole('button'));
    
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('禁用状态不应该触发点击', () => {
    const handleClick = vi.fn();
    render(<Button disabled onClick={handleClick}>禁用</Button>);
    
    fireEvent.click(screen.getByRole('button'));
    
    expect(handleClick).not.toHaveBeenCalled();
  });

  it('loading 状态应该显示加载指示器', () => {
    render(<Button loading>保存中</Button>);
    
    expect(screen.getByText('保存中')).toBeInTheDocument();
    expect(screen.getByTestId('spinner')).toBeInTheDocument();
  });

  it('应该支持不同的 variant', () => {
    const { rerender } = render(<Button variant="primary">主要</Button>);
    expect(screen.getByRole('button')).toHaveClass('btn-primary');

    rerender(<Button variant="danger">危险</Button>);
    expect(screen.getByRole('button')).toHaveClass('btn-danger');
  });
});

15.2 集成测试

typescript 复制代码
// __tests__/integration/UserForm.test.tsx
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import UserForm from '@/components/UserForm';

// Mock API
vi.mock('@/api/user', () => ({
  createUser: vi.fn(),
  updateUser: vi.fn(),
  getUserById: vi.fn()
}));

import * as userApi from '@/api/user';

function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: { retry: false, gcTime: 0 },
      mutations: { retry: false }
    }
  });
}

function renderWithProviders(ui: React.ReactElement) {
  const queryClient = createTestQueryClient();
  
  return render(
    <QueryClientProvider client={queryClient}>
      {ui}
    </QueryClientProvider>
  );
}

describe('UserForm 集成测试', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('编辑模式下应该预填充数据', async () => {
    const mockUser = {
      id: '1',
      name: '张三',
      email: 'zhangsan@example.com',
      role: 'admin'
    };

    vi.mocked(userApi.getUserById).mockResolvedValueOnce(mockUser);

    renderWithProviders(<UserForm userId="1" mode="edit" />);

    await waitFor(() => {
      expect(screen.getByDisplayValue('张三')).toBeInTheDocument();
      expect(screen.getByDisplayValue('zhangsan@example.com')).toBeInTheDocument();
    });
  });

  it('表单验证应该在必填字段为空时显示错误', async () => {
    renderWithProviders(<UserForm mode="create" />);

    fireEvent.click(screen.getByText('提交'));

    await waitFor(() => {
      expect(screen.getByText('请输入姓名')).toBeInTheDocument();
      expect(screen.getByText('请输入邮箱')).toBeInTheDocument();
    });
  });

  it('成功提交后应该显示成功提示并调用 API', async () => {
    vi.mocked(userApi.createUser).mockResolvedValueOnce({ success: true });

    renderWithProviders(<UserForm mode="create" />);

    fireEvent.change(screen.getByPlaceholderText('姓名'), {
      target: { value: '李四' }
    });
    fireEvent.change(screen.getByPlaceholderText('邮箱'), {
      target: { value: 'lisi@test.com' }
    });

    fireEvent.click(screen.getByText('提交'));

    await waitFor(() => {
      expect(userApi.createUser).toHaveBeenCalledWith({
        name: '李四',
        email: 'lisi@test.com'
      });
    });

    await waitFor(() => {
      expect(screen.getByText('创建成功')).toBeInTheDocument();
    });
  });

  it('API 失败时应该显示错误信息', async () => {
    vi.mocked(userApi.createUser).mockRejectedValueOnce(new Error('网络错误'));

    renderWithProviders(<UserForm mode="create" />);

    fireEvent.change(screen.getByPlaceholderText('姓名'), {
      target: { value: '王五' }
    });
    fireEvent.click(screen.getByText('提交'));

    await waitFor(() => {
      expect(screen.getByText(/网络错误/)).toBeInTheDocument();
    });
  });
});

15.3 E2E 测试示例

typescript 复制代码
// e2e/login.spec.ts
import { test, expect, Page } from '@playwright/test';

test.describe('登录流程', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('应该成功登录有效凭据', async ({ page }) => {
    await page.fill('[data-testid="email-input"]', 'admin@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="submit-button"]');

    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('[data-testid="welcome-message"]')).toContainText('欢迎');
  });

  test('无效凭据应该显示错误提示', async ({ page }) => {
    await page.fill('[data-testid="email-input"]', 'invalid@email.com');
    await page.fill('[data-testid="password-input"]', 'wrongpass');
    await page.click('[data-testid="submit-button"]');

    await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
    await expect(page).toHaveURL('/login');
  });

  test('空表单应该显示验证错误', async ({ page }) => {
    await page.click('[data-testid="submit-button"]');

    await expect(page.locator('[data-testid="email-error"]')).toContainText('请输入邮箱');
    await expect(page.locator('[data-testid="password-error"]']).toContainText('请输入密码');
  });

  test('记住我应该持久化登录状态', async ({ page }) => {
    await page.fill('[data-testid="email-input"]', 'admin@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.check('[data-testid="remember-me"]');
    await page.click('[data-testid="submit-button"]');

    await expect(page).toHaveURL('/dashboard');

    // 刷新页面后仍然保持登录状态
    await page.reload();
    await expect(page).toHaveURL('/dashboard');
  });
});

// e2e/crud.spec.ts
test.describe('用户 CRUD 操作', () => {
  test.beforeEach(async ({ page }) => {
    // 登录获取认证
    await page.goto('/login');
    await page.fill('[data-testid="email-input"]', 'admin@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="submit-button"]');
    await page.waitForURL('/users');
  });

  test('应该能够创建新用户', async ({ page }) => {
    await page.click('[data-testid="add-user-btn"]');
    
    await page.waitForSelector('[data-testid="user-form-modal"]');
    
    await page.fill('[name="username"]', 'testuser');
    await page.fill('[name="email"]', 'test@example.com');
    await page.selectOption('[name="role"]', 'editor');
    
    await page.click('[data-testid="save-btn"]');
    
    await expect(page.locator('[data-testid="success-toast"]')).toBeVisible();
    await expect(page.locator('text=testuser')).toBeVisible();
  });

  test('应该能够搜索和筛选用户', async ({ page }) => {
    await page.fill('[data-testid="search-input"]', 'admin');
    
    await expect(page.locator('.user-row')).toHaveCount(1);
    await expect(page.locator('.user-row').first()).toContainText('admin');
  });

  test('应该能够分页浏览用户列表', async ({ page }) => {
    await expect(page.locator('[data-testid="pagination"]')).toBeVisible();
    
    const currentPage = page.locator('[data-testid="current-page"]');
    await expect(currentPage).toHaveText('1');
    
    await page.click('[data-testid="next-page"]');
    await expect(currentPage).not.toHaveText('1');
  });
});

十六、SSR 与 Next.js 进阶

16.1 服务端数据获取

typescript 复制代码
// app/posts/[id]/page.tsx (Next.js App Router)
import { notFound } from 'next/navigation';
import { cache } from 'react';

// 缓存数据获取函数,避免重复请求
const getPost = cache(async (id: string) => {
  const res = await fetch(`${process.env.API_URL}/posts/${id}`, {
    next: { revalidate: 3600, tags: ['post'] } // ISR:每小时重新验证
  });

  if (!res.status === 404) notFound();
  
  return res.json();
});

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await getPost(params.id);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
      <PostMetadata 
        author={post.author}
        createdAt={post.createdAt}
        views={post.views}
      />
    </article>
  );
}

// 生成静态参数(用于 SSG)
export async function generateStaticParams() {
  const posts = await fetch(`${process.env.API_URL}/posts`).then(r => r.json());
  
  return posts.map((post: any) => ({
    id: post.id.toString()
  }));
}

// 动态元数据
export async function generateMetadata({ params }: { params: { id: string } }) {
  const post = await getPost(params.id);
  
  return {
    title: `${post.title} - 我的博客`,
    description: post.excerpt,
    openGraph: {
      images: [post.coverImage],
      type: 'article',
      publishedTime: post.createdAt,
      authors: [post.author.name]
    },
    alternates: {
      canonical: `/posts/${params.id}`
    }
  };
}

16.2 客户端与服务端组件分离

tsx 复制代码
// components/PostContent.server.tsx - 服务端组件(默认)
import { Suspense } from 'react';
import PostComments from './PostComments.client';

interface PostContentProps {
  postId: string;
}

async function PostContent({ postId }: PostContentProps) {
  // 服务端数据获取
  const post = await fetchPost(postId);
  const relatedPosts = await fetchRelatedPosts(postId);

  return (
    <div className="post-content">
      <header>
        <h1>{post.title}</h1>
        <time dateTime={post.createdAt}>
          {formatDate(post.createdAt)}
        </time>
      </header>

      <section className="markdown-body">
        <MarkdownRenderer content={post.content} />
      </section>

      {/* 相关文章 - 服务端渲染 */}
      <aside className="related-posts">
        <h2>相关推荐</h2>
        <ul>
          {relatedPosts.map(p => (
            <li key={p.id}>
              <Link href={`/posts/${p.id}`}>{p.title}</Link>
            </li>
          ))}
        </ul>
      </aside>

      {/* 评论区 - 客户端组件 */}
      <Suspense fallback={<CommentSkeleton />}>
        <PostComments postId={postId} />
      </Suspense>
    </div>
  );
}

// components/PostComments.client.tsx - 客户端组件
'use client';

import { useState, useCallback } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function PostComments({ postId }: { postId: string }) {
  const queryClient = useQueryClient();
  const [replyTo, setReplyTo] = useState<string | null>(null);

  // 客户端数据获取
  const { data: comments, isLoading } = useQuery({
    queryKey: ['comments', postId],
    queryFn: () => api.getComments(postId),
    staleTime: 5 * 60 * 1000
  });

  const addMutation = useMutation({
    mutationFn: (content: string) => api.addComment(postId, content),
    onSuccess: () => {
      queryClient.invalidateQueries(['comments', postId]);
      setReplyTo(null);
    }
  });

  return (
    <section className="comments">
      <h2>评论 ({comments?.length ?? 0})</h2>

      {isLoading ? (
        <CommentSkeleton />
      ) : (
        <ul className="comment-list">
          {comments?.map(comment => (
            <CommentItem
              key={comment.id}
              comment={comment}
              onReply={() => setReplyTo(comment.id)}
            />
          ))}
        </ul>
      )}

      <CommentForm
        replyTo={replyTo}
        onSubmit={(content) => addMutation.mutate(content)}
      />
    </section>
  );
}

16.3 Server Actions

typescript 复制代码
// actions/post.actions.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { auth } from '@/auth';
import { z } from 'zod';

const createPostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
  category: z.enum(['tech', 'life', 'tutorial']),
  tags: z.array(z.string()).max(5),
  isDraft: z.boolean().default(false)
});

export async function createPost(formData: FormData) {
  // 服务端验证
  const validatedFields = createPostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
    category: formData.get('category'),
    tags: formData.getAll('tags'),
    isDraft: formData.get('isDraft') === 'on'
  });

  if (!validatedFields.success) {
    return {
      errors: validatedErrors.flatten().fieldErrors,
      message: '字段验证失败'
    };
  }

  // 认证检查
  const session = await auth();
  if (!session?.user) {
    return { error: '未授权' };
  }

  try {
    const post = await db.post.create({
      data: {
        ...validatedFields.data,
        authorId: session.user.id,
        slug: generateSlug(validatedFields.data.title)
      }
    });

    // 增量静态重新验证(ISR)
    revalidateTag('posts');
    
    if (!validatedFields.data.isDraft) {
      redirect(`/posts/${post.slug}`);
    }

    return { success: true, post };
  } catch (error) {
    console.error('创建文章失败:', error);
    return { error: '创建失败' };
  }
}

export async function updatePost(
  id: string,
  prevState: any,
  formData: FormData
) {
  const session = await auth();
  
  // 权限检查
  const existingPost = await db.post.findUnique({ where: { id } });
  if (!existingPost || existingPost.authorId !== session.user.id) {
    return { error: '无权修改' };
  }

  const updatedData = {
    title: formData.get('title') as string,
    content: formData.get('content') as string,
    updatedAt: new Date()
  };

  await db.post.update({
    where: { id },
    data: updatedData
  });

  revalidatePath(`/posts/${id}`);
  revalidatePath('/admin/posts');

  return { success: true };
}

// 使用
// app/admin/posts/new/page.tsx
import { createPost } from '@/actions/post.actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="标题" />
      <textarea name="content" placeholder="内容" />
      
      <select name="category">
        <option value="tech">技术</option>
        <option value="life">生活</option>
        <option value="tutorial">教程</option>
      </select>

      <input type="checkbox" name="isDraft" />
      <label>保存为草稿</label>

      <button type="submit">发布</button>
    </form>
  );
}

16.4 中间件与路由保护

typescript 复制代码
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyToken } from '@/lib/auth';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 公开路径白名单
  const publicPaths = ['/login', '/register', '/forgot-password', '/api/auth'];
  const isPublicPath = publicPaths.some(path => pathname.startsWith(path));

  // 静态资源跳过
  const staticAssets = /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$/;
  if (staticAssets.test(pathname)) {
    return NextResponse.next();
  }

  // API 路由认证
  if (pathname.startsWith('/api/')) {
    const token = request.headers.get('authorization')?.replace('Bearer ', '');
    
    if (!token && !isPublicPath) {
      return NextResponse.json(
        { error: '未授权' },
        { status: 401 }
      );
    }

    if (token) {
      const payload = verifyToken(token);
      if (!payload) {
        return NextResponse.json(
          { error: 'Token 无效' },
          { status: 401 }
        );
      }

      // 将用户信息注入请求头
      const requestHeaders = new Headers(request.headers);
      requestHeaders.set('x-user-id', payload.userId);

      return NextResponse.next({
        request: { headers: requestHeaders }
      });
    }
  }

  // 页面路由认证
  if (!isPublicPath) {
    const token = request.cookies.get('auth-token')?.value;

    if (!token) {
      const loginUrl = new URL('/login', request.url);
      loginUrl.searchParams.set('redirect', pathname);
      return NextResponse.redirect(loginUrl);
    }

    const payload = verifyToken(token);
    if (!payload) {
      const response = NextResponse.redirect(new URL('/login', request.url));
      response.cookies.delete('auth-token');
      return response;
    }

    // 管理员路由权限检查
    if (pathname.startsWith('/admin') && payload.role !== 'admin') {
      return NextResponse.redirect(new URL('/', request.url));
    }
  }

  // 安全头设置
  const response = NextResponse.next();
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

  return response;
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico).*)'
  ]
};

十七、调试与性能监控

17.1 React DevTools Profiler 集成

typescript 复制代码
// utils/profiler.ts
import { Profiler, type OnRenderCallback } from 'react';

interface MeasureConfig {
  id: string;
  phase: 'mount' | 'update';
  actualDuration: number;
  baseDuration: number;
  startTime: number;
  commitTime: number;
  interactions: Set<any>;
}

const onRenderCallback: OnRenderCallback = (
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime,
  interactions
) => {
  // 性能阈值警告
  if (actualDuration > 100) {
    console.warn(`[Performance] ${id} 渲染耗时 ${actualDuration.toFixed(2)}ms`);
  }

  // 上报到监控系统
  if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
    sendMetrics({
      component: id,
      phase,
      duration: actualDuration,
      timestamp: Date.now(),
      url: window.location.pathname
    });
  }
};

// 性能监控 HOC
export function withProfiler<P extends object>(
  WrappedComponent: ComponentType<P>,
  componentName?: string
) {
  const displayName = componentName || WrappedComponent.displayName || WrappedComponent.name || 'Unknown';

  return function ProfiledComponent(props: P) {
    return (
      <Profiler id={displayName} onRender={onRenderCallback}>
        <WrappedComponent {...props} />
      </Profiler>
    );
  };
}

// 使用
const HeavyTable = withProfiler(DataTable, 'HeavyTable');
const ComplexForm = withProfiler(UserForm, 'UserForm');

17.2 自定义错误边界与日志

tsx 复制代码
// components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { captureException } from '@sentry/nextjs';

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
  resetKeys?: any[];
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // 错误上报到 Sentry
    captureException(error, {
      contexts: {
        react: {
          componentStack: errorInfo.componentStack
        }
      },
      tags: {
        type: 'error-boundary',
        location: typeof window !== 'undefined' ? window.location.href : 'unknown'
      }
    });

    this.props.onError?.(error, errorInfo);
  }

  componentDidUpdate(prevProps: ErrorBoundaryProps) {
    // resetKeys 变化时重置状态
    if (
      this.props.resetKeys &&
      prevProps.resetKeys &&
      this.props.resetKeys.some((key, i) => key !== prevProps.resetKeys[i])
    ) {
      this.setState({ hasError: false, error: null });
    }
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div className="error-fallback">
          <div className="error-icon">⚠️</div>
          <h2>出错了</h2>
          <p>{this.state.error?.message}</p>
          
          {process.env.NODE_ENV === 'development' && (
            <details className="error-details">
              <summary>错误详情</summary>
              <pre>{this.state.error?.stack}</pre>
            </details>
          )}

          <div className="error-actions">
            <button onClick={() => window.location.reload()}>
              刷新页面
            </button>
            <button onClick={() => this.setState({ hasError: false, error: null })}>
              重试
            </button>
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}

// 全局错误处理
// app/error.tsx (Next.js App Router)
'use client';

export default function GlobalError({
  error,
  reset
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    console.error('Global error:', error);
  }, [error]);

  return (
    <html lang="zh-CN">
      <body>
        <div className="global-error">
          <h1>应用出错</h1>
          <p>抱歉,发生了意外错误。</p>
          <button onClick={reset}>重新尝试</button>
        </div>
      </body>
    </html>
  );
}

17.3 性能指标收集

typescript 复制代码
// utils/performance.ts
type MetricName =
  | 'FCP'
  | 'LCP'
  | 'FID'
  | 'CLS'
  | 'TTFB'
  | 'INP';

interface PerformanceMetric {
  name: MetricName;
  value: number;
  rating: 'good' | 'needs-improvement' | 'poor';
  timestamp: number;
  url: string;
  userAgent: string;
}

class PerformanceMonitor {
  private metrics: PerformanceMetric[] = [];
  private endpoint: string;

  constructor(endpoint?: string) {
    this.endpoint = endpoint ?? '/api/metrics';
    this.init();
  }

  private init() {
    if (typeof window === 'undefined') return;

    // Web Vitals 监听
    import('web-vitals').then(({ onFCP, onLCP, onFID, onCLS, onTTFB, onINP }) => {
      onFCP(this.reportMetric.bind(this));
      onLCP(this.reportMetric.bind(this));
      onFID(this.reportMetric.bind(this));
      onCLS(this.reportMetric.bind(this));
      onTTFB(this.reportMetric.bind(this));
      onINP(this.reportMetric.bind(this));
    });

    // 资源加载时间
    this.observeResourceTiming();

    // 自定义用户交互计时
    this.setupInteractionObserver();
  }

  private reportMetric(metric: any) {
    const performanceMetric: PerformanceMetric = {
      name: metric.name,
      value: metric.value,
      rating: metric.rating,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent
    };

    this.metrics.push(performanceMetric);
    this.sendToServer(performanceMetric);

    // 控制台输出(开发环境)
    if (process.env.NODE_ENV === 'development') {
      console.log(
        `[Web Vital] ${metric.name}: ${metric.value} (${metric.rating})`
      );
    }
  }

  private observeResourceTiming() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'resource') {
          const resource = entry as PerformanceResourceTiming;
          
          // 慢资源警告
          if (resource.duration > 3000) {
            console.warn(
              `[Slow Resource] ${resource.name} took ${resource.duration.toFixed(0)}ms`
            );

            this.sendToServer({
              name: 'SLOW_RESOURCE',
              value: resource.duration,
              rating: 'poor',
              timestamp: Date.now(),
              url: resource.name,
              userAgent: navigator.userAgent
            });
          }
        }
      }
    });

    observer.observe({ entryTypes: ['resource'] });
  }

  private setupInteractionObserver() {
    let interactionStart = 0;

    document.addEventListener('pointerdown', () => {
      interactionStart = performance.now();
    }, { passive: true });

    document.addEventListener('click', () => {
      const duration = performance.now() - interactionStart;
      
      if (duration > 100) {
        this.sendToServer({
          name: 'CLICK_LATENCY',
          value: duration,
          rating: duration > 500 ? 'poor' : 'needs-improvement',
          timestamp: Date.now(),
          url: window.location.href,
          userAgent: navigator.userAgent
        });
      }
    });
  }

  private async sendToServer(metric: PerformanceMetric) {
    if (navigator.sendBeacon) {
      navigator.sendBeacon(
        this.endpoint,
        JSON.stringify(metric)
      );
    } else {
      try {
        await fetch(this.endpoint, {
          method: 'POST',
          body: JSON.stringify(metric),
          keepalive: true
        });
      } catch {
        // 静默失败,不影响用户体验
      }
    }
  }

  // 自定义标记
  mark(name: string) {
    performance.mark(`${name}-start`);
  }

  measure(name: string) {
    performance.mark(`${name}-end`);
    performance.measure(name, `${name}-start`, `${name}-end`);
    
    const measure = performance.getEntriesByName(name).pop();
    if (measure) {
      this.sendToServer({
        name: name.toUpperCase() as MetricName,
        value: measure.duration,
        rating: measure.duration < 100 ? 'good' : 'needs-improvement',
        timestamp: Date.now(),
        url: window.location.href,
        userAgent: navigator.userAgent
      });
    }
  }

  getMetrics(): PerformanceMetric[] {
    return [...this.metrics];
  }
}

export const perfMonitor = new PerformanceMonitor();

// 使用
function DataFetchingExample() {
  useEffect(() => {
    perfMonitor.mark('user-data-fetch');
    
    fetchData().then(() => {
      perfMonitor.measure('user-data-fetch');
    });
  }, []);

  return null;
}

17.4 开发环境调试工具

typescript 复制代码
// devtools/ReactDevToolsHelper.ts
declare global {
  interface Window {
    __REACT_DEVTOOLS_GLOBAL_HOOK__: any;
  }
}

// 开发模式增强
if (process.env.NODE_ENV === 'development') {
  // 组件挂载计数器
  const mountCounts = new Map<string, number>();

  const originalCreateElement = React.createElement;
  React.createElement = function(type: any, props: any, ...children: any[]) {
    if (typeof type === 'function' && type.displayName) {
      const count = mountCounts.get(type.displayName) ?? 0;
      mountCounts.set(type.displayName, count + 1);
    }

    return originalCreateElement.call(this, type, props, ...children);
  };

  // 暴露调试工具
  window.__REACT_DEBUG__ = {
    getMountCounts: () => Object.fromEntries(mountCounts),
    resetCounts: () => mountCounts.clear(),
    findComponentsByName: (name: string) => {
      const elements = document.querySelectorAll('[data-reactroot]');
      return Array.from(elements).filter(el => el.textContent?.includes(name));
    }
  };

  // 性能警告
  const warnIfOverRendered = (() => {
    const renderCounts = new WeakMap<object, number>();
    
    return (component: any) => {
      const count = renderCounts.get(component) ?? 0;
      renderCounts.set(component, count + 1);
      
      if (count > 50 && count % 20 === 0) {
        console.warn(
          `⚠️ ${component.constructor?.name || 'Anonymous'} 已渲染 ${count + 1} 次`,
          component
        );
      }
    };
  })();
}

// 自定义 Hook 调试
function useDebugValue<T>(value: T, formatter?: (value: T) => string) {
  const formatted = formatter ? formatter(value) : String(value);
  
  if (process.env.NODE_ENV === 'development') {
    // 在 DevTools 中显示格式化的值
    React.useDebugValue(formatted);
  }

  return value;
}

// 使用
function UserProfile({ userId }: { userId: string }) {
  const user = useQuery(['user', userId], () => api.getUser(userId));
  
  useDebugValue(user.data, user =>
    user ? `${user.name} (${user.role})` : 'Loading...'
  );

  return null;
}

十八、微前端架构

18.1 Module Federation 配置

javascript 复制代码
// next.config.js (Host 应用)
const ModuleFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config, options) {
    if (!options.isServer) {
      config.plugins.push(
        new ModuleFederationPlugin({
          name: 'host',
          filename: 'remoteEntry.js',
          remotes: {
            dashboard: 'dashboard@http://localhost:3001/_next/static/chunks/remoteEntry.js',
            settings: 'settings@http://localhost:3002/_next/static/charts/remoteEntry.js',
            crm: 'crm@http://localhost:3003/_next/static/charts/remoteEntry.js'
          },
          shared: {
            react: { singleton: true, requiredVersion: '^18.0.0' },
            'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
            '@tanstack/react-query': { singleton: true },
            zustand: { singleton: true },
            'lodash-es': { singleton: false }
          }
        })
      );
    }

    return config;
  }
};

// next.config.js (Remote 应用 - Dashboard)
module.exports = {
  webpack(config, options) {
    config.plugins.push(
      new ModuleFederationPlugin({
        name: 'dashboard',
        filename: 'remoteEntry.js',
        exposes: {
          './Dashboard': './components/Dashboard',
          './StatsCard': './components/StatsCard',
          './Chart': './components/Chart',
          './useDashboardData': './hooks/useDashboardData'
        },
        shared: {
          react: { singleton: true, requiredVersion: '^18.0.0' },
          'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
        }
      })
    );

    return config;
  }
};

18.2 远程组件动态加载

tsx 复制代码
// components/DynamicRemote.tsx
import React, { useState, useEffect, Suspense, lazy } from 'react';

interface DynamicRemoteProps {
  remote: string;       // remote 名称
  module: string;       // 暴露的模块路径
  fallback?: React.ReactNode;
  props?: Record<string, any>;
  onError?: (error: Error) => void;
}

export function DynamicRemote({
  remote,
  module,
  fallback = <RemoteLoader />,
  props = {},
  onError
}: DynamicRemoteProps) {
  const [Component, setComponent] = useState<React.ComponentType<any> | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let mounted = true;

    async function loadRemote() {
      try {
        setLoading(true);
        
        // 动态导入远程模块
        const RemoteComponent = lazy(() =>
          import(/* webpackIgnore: true */ `remote/${remote}/${module}`)
        );

        if (mounted) {
          setComponent(() => RemoteComponent);
          setLoading(false);
        }
      } catch (err) {
        if (mounted) {
          setError(err as Error);
          setLoading(false);
          onError?.(err as Error);
        }
      }
    }

    loadRemote();

    return () => { mounted = false; };
  }, [remote, module]);

  if (error) {
    return (
      <div className="remote-error">
        <p>模块加载失败</p>
        <button onClick={() => window.location.reload()}>
          刷新页面
        </button>
      </div>
    );
  }

  return (
    <Suspense fallback={fallback}>
      {Component && <Component {...props} />}
    </Suspense>
  );
}

// 使用
function HostApp() {
  return (
    <Layout>
      <Header />
      
      <main>
        <DynamicRemote
          remote="dashboard"
          module="./Dashboard"
          props={{ userId: currentUser.id }}
          onError={(err) => trackError(err)}
        />

        <DynamicRemote
          remote="settings"
          module="./SettingsPanel"
          fallback={<SettingsSkeleton />}
        />
      </main>

      <Footer />
    </Layout>
  );
}

18.3 微前端通信总线

typescript 复制代码
// microfrontends/eventBus.ts
type EventPayload = any;
type EventCallback = (payload: EventPayload) => void;

class MicroEventBus {
  private events = new Map<string, Set<EventCallback>>();
  private history: Array<{ event: string; payload: EventPayload; timestamp: number }> = [];

  // 发布事件
  emit(event: string, payload?: EventPayload, options?: { persist?: boolean }) {
    const callbacks = this.events.get(event);
    
    if (callbacks) {
      callbacks.forEach(callback => {
        try {
          callback(payload);
        } catch (error) {
          console.error(`Event handler error for "${event}":`, error);
        }
      });
    }

    // 记录事件历史
    this.history.push({ event, payload, timestamp: Date.now() });
    
    // 限制历史记录数量
    if (this.history.length > 100) {
      this.history.shift();
    }

    // 持久化重要事件
    if (options?.persist) {
      sessionStorage.setItem(
        `mfe-event-${event}`,
        JSON.stringify({ payload, timestamp: Date.now() })
      );
    }
  }

  // 订阅事件
  subscribe(event: string, callback: EventCallback): () => void {
    if (!this.events.has(event)) {
      this.events.set(event, new Set());
    }

    this.events.get(event)!.add(callback);

    // 返回取消订阅函数
    return () => {
      this.events.get(event)?.delete(callback);
    };
  }

  // 只订阅一次
  once(event: string, callback: EventCallback): () => void {
    const wrapper = (payload: EventPayload) => {
      callback(payload);
      this.unsubscribe(event, wrapper);
    };

    return this.subscribe(event, wrapper);
  }

  // 取消订阅
  unsubscribe(event: string, callback?: EventCallback) {
    if (callback) {
      this.events.get(event)?.delete(callback);
    } else {
      this.events.delete(event);
    }
  }

  // 获取持久化的事件
  getPersistedEvent<T>(event: string): T | null {
    try {
      const stored = sessionStorage.getItem(`mfe-event-${event}`);
      return stored ? JSON.parse(stored).payload : null;
    } catch {
      return null;
    }
  }

  // 清除持久化事件
  clearPersistedEvent(event: string) {
    sessionStorage.removeItem(`mfe-event-${event}`);
  }

  // 获取事件历史
  getHistory(event?: string): typeof this.history {
    if (event) {
      return this.history.filter(e => e.event === event);
    }
    return [...this.history];
  }
}

// 全局单例
export const microEventBus = new MicroEventBus();

// 类型安全的事件定义
interface MFEEvents {
  'user:login': { userId: string; token: string };
  'user:logout': {};
  'theme:change': { theme: 'light' | 'dark' };
  'notification:show': { type: 'success' | 'error' | 'info'; message: string };
  'route:change': { path: string; from: string };
}

// 类型安全的包装器
export function createTypedEventBus<E extends Record<string, any>>() {
  return {
    emit<K extends keyof E>(event: K, payload: E[K]) {
      microEventBus.emit(event as string, payload);
    },
    
    subscribe<K extends keyof E>(
      event: K,
      callback: (payload: E[K]) => void
    ) {
      return microEventBus.subscribe(event as string, callback);
    },

    once<K extends keyof E>(
      event: K,
      callback: (payload: E[K]) => void
    ) {
      return microEventBus.once(event as string, callback);
    }
  };
}

export const typedEventBus = createTypedEventBus<MFEEvents>();

// 使用示例
// Dashboard 微应用
typedEventBus.subscribe('user:login', ({ userId }) => {
  loadUserData(userId);
});

// Settings 微应用
function ThemeSwitcher() {
  const changeTheme = (theme: 'light' | 'dark') => {
    typedEventBus.emit('theme:change', { theme });
  };

  return (
    <button onClick={() => changeTheme('dark')}>
      切换暗色主题
    </button>
  );
}

18.4 共享状态管理

typescript 复制代码
// microfrontends/sharedStore.ts
import { createStore } from 'zustand/vanilla';

// 跨应用共享的 Store
interface SharedStore {
  // 用户信息
  user: User | null;
  setUser: (user: User | null) => void;

  // 主题
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;

  // 全局通知
  notifications: Notification[];
  addNotification: (notification: Omit<Notification, 'id'>) => void;
  removeNotification: (id: string) => void;

  // 导航状态
  sidebarOpen: boolean;
  toggleSidebar: () => void;
}

export const sharedStore = createStore<SharedStore>((set, get) => ({
  user: null,
  setUser: (user) => set({ user }),

  theme: 'light',
  setTheme: (theme) => {
    set({ theme });
    document.documentElement.setAttribute('data-theme', theme);
    localStorage.setItem('theme', theme);
  },

  notifications: [],
  addNotification: (notification) => {
    const id = crypto.randomUUID();
    set(state => ({
      notifications: [...state.notifications, { ...notification, id }]
    }));

    // 自动移除
    setTimeout(() => {
      get().removeNotification(id);
    }, 5000);
  },
  removeNotification: (id) => {
    set(state => ({
      notifications: state.notifications.filter(n => n.id !== id)
    }));
  },

  sidebarOpen: true,
  toggleSidebar: () => set(state => ({ sidebarOpen: !state.sidebarOpen }))
}));

// React Hook 封装
import { useStore } from 'zustand';

export function useSharedStore() {
  return useStore(sharedStore);
}

// 选择性订阅(性能优化)
export function useTheme() {
  return useSharedStore(state => state.theme);
}

export function useNotifications() {
  return useSharedStore(state => state.notifications);
}

// 初始化共享 Store
export function initSharedStore() {
  const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
  if (savedTheme) {
    sharedStore.getState().setTheme(savedTheme);
  }

  // 同步用户登录状态
  const persistedUser = sessionStorage.getItem('user');
  if (persistedUser) {
    try {
      sharedStore.setState({ user: JSON.parse(persistedUser) });
    } catch {
      // 忽略解析错误
    }
  }
}
相关推荐
派小心.几秒前
DAU和MAU怎么计算?先确定活跃事件、用户键和统计窗口
大数据·服务器·前端·人工智能·数据分析
代码猎人1 分钟前
前端怎么配合后端完成bug修复
前端
定栓5 分钟前
Webpack迁到Vite第七节(四):全家桶移除 — 保留项、脚本与数据收尾
前端·webpack·node.js
代码猎人13 分钟前
前端怎么保障项目稳定上线与运行
前端
数据掘金14 分钟前
小程序埋点方案的埋点文档怎么自动生成?
前端
deli00714 分钟前
cron 表达式为什么总跑错?10 个场景实测,下次执行时间一眼看懂
前端
代码猎人18 分钟前
前端怎么优化页面性能
前端
代码猎人20 分钟前
前端怎么配合后端完成数据对接
前端
lexiangqicheng26 分钟前
Docker的沙箱机制是怎样的?
前端·javascript
燕落南枝1 小时前
HTML 表格标签与 AI 合并单元格实战指南
前端·人工智能·html