mcpField
R
react-patterns

react-patterns

github.com/dralgorhythm开发12964 次安装15 周前更新
代码仓库

关于 react-patterns

面向使用 TypeScript 的 React 开发者,介绍组件组合模式、React 19 特性及可维护应用的实践,适用于 Web 和移动端 React 开发。

主要功能

  1. 01组件组合模式
  2. 02属性与事件的类型定义
  3. 03可复用自定义 Hook
  4. 04React 19 Hook 与特性
  5. 05状态和性能模式
  6. 06Suspense 与错误边界

适用场景

  1. 01设计可复用的组件 API
  2. 02在组件间共享逻辑
  3. 03管理复杂 UI 状态
  4. 04检查加载与错误处理行为

MCP 相关

README 建议使用 Context7 MCP 查询完整的 React API 参考。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dralgorhythm/claude-agentic-framework --skill react-patterns

安装后在 Claude Code 中输入/react-patterns确认已加载。

详细介绍

React 模式

平台: Web 和移动端(共享的 React 模式)。有关 React Native 特定模式(Pressable、ScrollView、FlashList、安全区域),请参阅 react-native-patterns 技能。

概述

使用 TypeScript 构建易于维护的 React 应用,利用 React 19 特性和组合模式。

API 参考: 使用 Context7 MCP 查阅完整的 React API 参考(mcp__context7__resolve-library-id → react)。

工作流

  • 选择合适的组件组合模式
  • 为属性和事件应用 TypeScript 类型
  • 为共享逻辑实现自定义 Hook
  • 在需要时添加性能优化
  • 使用 Suspense/边界处理加载和错误状态
  • 验证组件渲染行为

反馈循环

  • 组件渲染时没有 TypeScript 错误
  • 属性已正确标注类型并经过验证
  • 自定义 Hook 具有明确的返回类型
  • 没有不必要的重复渲染(使用 React DevTools Profiler)
  • 错误边界能够捕获组件错误
  • 加载状态可与 Suspense 正常配合

参考实现

1. 组件组合
复合组件
// Shares implicit state between parent and children
const TabsContext = createContext<{ activeTab: string; setActiveTab: (id: string) => void } | null>(null);

function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  return <TabsContext.Provider value={{ activeTab, setActiveTab }}>{children}</TabsContext.Provider>;
}

function Tab({ id, children }: { id: string; children: ReactNode }) {
  const ctx = use(TabsContext);
  if (!ctx) throw new Error('Tab must be used within Tabs');
  return (
    <button role="tab" aria-selected={ctx.activeTab === id} onClick={() => ctx.setActiveTab(id)}>
      {children}
    </button>
  );
}
插槽模式
// Named slots for flexible composition
interface CardProps { header?: ReactNode; footer?: ReactNode; children: ReactNode; }

function Card({ header, footer, children }: CardProps) {
  return (
    <div className="card">
      {header && <div className="card-header">{header}</div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}
2. React 19 特性
use() Hook
// Unwrap promises and context — simpler than useContext
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // Suspends until resolved
  return <div>{user.name}</div>;
}
useActionState
async function updateUser(prevState: { error?: string }, formData: FormData) {
  'use server';
  return { error: undefined };
}

function UserForm() {
  const [state, formAction, isPending] = useActionState(updateUser, {});
  return (
    <form action={formAction}>
      <input name="name" disabled={isPending} />
      {state.error && <p className="error">{state.error}</p>}
      <button disabled={isPending}>{isPending ? 'Saving...' : 'Save'}</button>
    </form>
  );
}
useOptimistic
function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo: Todo) => [...state, newTodo]
  );
  async function handleAdd(formData: FormData) {
    const todo = { id: crypto.randomUUID(), text: formData.get('text') as string };
    addOptimisticTodo(todo);
    await saveTodo(todo);
  }
  return (
    <form action={handleAdd}>
      {optimisticTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}
      <input name="text" /><button>Add</button>
    </form>
  );
}
3. 自定义 Hook
// Object return for multiple named values
function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const login = async (credentials: Credentials) => { setUser(await api.login(credentials)); };
  return { user, loading, login };
}

// Tuple return for positional access (like useState)
function useToggle(initial = false): [boolean, () => void] {
  const [value, setValue] = useState(initial);
  return [value, useCallback(() => setValue(v => !v), [])];
}

// Composing hooks
function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initial;
  });
  useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]);
  return [value, setValue] as const;
}
4. TypeScript + React
// Extend HTML element props
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary';
  loading?: boolean;
}
function Button({ variant = 'primary', loading, children, ...props }: ButtonProps) {
  return <button className={variant} disabled={loading} {...props}>{loading ? 'Loading...' : children}</button>;
}

// Generic components with full type inference
interface ListProps<T> { items: T[]; renderItem: (item: T) => ReactNode; keyExtractor: (item: T) => string; }
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return <ul>{items.map(item => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
}

// Refs as props (React 19+) — forwardRef is deprecated
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  ref?: React.Ref<HTMLInputElement>;
}
function Input({ label, ref, ...props }: InputProps) {
  return <label>{label}<input ref={ref} {...props} /></label>;
}
5. 状态管理
使用 useReducer 管理复杂状态
type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: User[] }
  | { type: 'FETCH_ERROR'; payload: string };

function reducer(state: { data: User[]; loading: boolean; error: string | null }, action: Action) {
  switch (action.type) {
    case 'FETCH_START': return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS': return { ...state, loading: false, data: action.payload };
    case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload };
  }
}
6. 性能模式
// memo + useCallback prevent unnecessary re-renders
const ListItem = memo(function ListItem({ item, onDelete }: ItemProps) {
  return <li>{item.name}<button onClick={() => onDelete(item.id)}>Delete</button></li>;
});

function List() {
  const [items, setItems] = useState<Item[]>([]);
  const handleDelete = useCallback((id: string) => {
    setItems(items => items.filter(item => item.id !== id));
  }, []);
  return <>{items.map(item => <ListItem key={item.id} item={item} onDelete={handleDelete} />)}</>;
}

// Lazy loading with Suspense
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes><Route path="/dashboard" element={<Dashboard />} /></Routes>
    </Suspense>
  );
}
7. 错误处理
class ErrorBoundary extends Component<{ children: ReactNode; fallback?: ReactNode }, { hasError: boolean }> {
  state = { hasError: false };
  static getDerivedStateFromError(): { hasError: boolean } { return { hasError: true }; }
  componentDidCatch(error: Error, info: ErrorInfo) { console.error(error, info); }
  render() {
    return this.state.hasError ? (this.props.fallback ?? <div>Something went wrong</div>) : this.props.children;
  }
}

// Wrap async data loading with Suspense
function UserProfile({ userId }: { userId: string }) {
  const user = use(fetchUser(userId)); // Suspends
  return <div>{user.name}</div>;
}

最佳实践

  • 组合优于继承 - 使用组合模式提升灵活性
  • 为所有内容标注类型 - 利用 TypeScript 在编译时确保安全
  • 就近放置状态 - 尽可能让状态靠近其使用位置
  • 提取自定义 Hook - 使用 Hook 在组件间共享逻辑
  • 保持引用稳定 - 使用 useCallback/useMemo 避免不必要的重复渲染
  • 错误边界 - 包裹组件树以捕获渲染错误
  • 使用 Suspense 处理加载 - 使用 Suspense,而非手动管理加载状态
  • 避免属性逐层传递 - 对于深层嵌套的属性,使用上下文或组合

反模式

  • 在 React 19 中使用 forwardRef - 改用普通属性 ref
  • 为新代码使用类组件 - 使用函数组件和 Hook
  • 直接修改状态 - 始终使用 setState 或 reducer 操作
  • 缺少依赖数组 - 在 useEffect/useMemo/useCallback 中包含所有依赖项
  • 过度使用 useMemo/useCallback - 仅在必要时优化(先进行性能分析)
  • 所有情况都使用上下文 - 谨慎使用上下文;优先使用属性或状态管理库
  • 在 useState 中存储派生状态 - 改为在渲染期间计算派生值
  • 使用 useEffect 处理派生状态 - 使用 useMemo,或直接在渲染期间计算
  • 将索引用作 key - 为列表 key 使用稳定且唯一的 ID
  • 忽略 TypeScript 错误 - 绝不要用 'any' 或 '// @ts-ignore' 走捷径

评论

支持 Markdown