- AI 技能
- AI 插件
【免费下载链接】agentic-awesome-skills
AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,445+ agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.
本篇指南以 AAS(agentic-awesome-skills)仓库中的cc-skill-frontend-patterns技能为核心,系统讲解其在 React、Next.js 场景下沉淀的组件组合、自定义 Hooks、状态管理、性能优化、表单处理、错误边界、动画与无障碍八大类前端模式。读完本文,你将掌握一套可直接复用的 TypeScript/React 代码范式,并能理解该技能在仓库中的元数据定位、触发方式与执行约束,从而在自己项目中按需落地。
技能定位:一个专攻前端开发模式的社区技能
cc-skill-frontend-patterns是 AAS 仓库中一个面向社区贡献、风险等级为critical的元技能(meta 技能),其完整定义位于 SKILL.md(仓库plugins/agentic-awesome-skills目录下另有一份相同内容的副本)。从 data/catalog.json 中的目录条目可以看到它的完整元信息:
- id / canonical_id:
cc-skill-frontend-patterns - description:Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices
- category:
meta;risk:critical;source:community - tags:
cc、skill、frontend - triggers:
cc、skill、frontend、development、react、next、js、state、performance、optimization、ui
在 AAS 的技能体系中,技能通过 YAML frontmatter 声明name、description、risk、source、date_added等元数据(该技能添加于2026-02-27),目录索引(catalog.json)据此为 Agent 提供"技能发现"能力。技能主体SKILL.md只保留激活条件、示例与约束,而把完整过程与参考材料放在references/detailed-guide.md中——这正是仓库 docs/contributors/skill-anatomy.md 与 docs/contributors/skill-template.md 所描述的"Skill Anatomy"推荐结构:SKILL.md 负责"何时用",详细指南负责"怎么用"。
何时使用本技能
按照 SKILL.md 的定义,本技能适用于"执行概览中所描述的工作流或动作"的任务,具体来说:
- 开发或重构 React / Next.js 组件库与页面;
- 设计跨组件共享的 Hooks 与状态管理层;
- 需要对列表、图表等重渲染场景做性能优化;
- 编写带校验的表单、弹窗、下拉等交互组件,并保证可访问性。
使用限制(Limitations)
SKILL.md 同时明确了三条硬性边界:
- 仅在任务与上述范围清晰匹配时使用,不要套用到无关领域;
- 技能输出不能替代针对具体环境的验证、测试或专家评审;
- 当缺少必要输入、权限、安全边界或成功标准时,应停下来向用户澄清,而不是擅自执行。
组件模式:组合优于继承
详细指南(references/detailed-guide.md)的第一部分讨论组件设计。核心思想是:用组合(composition)而不是继承(inheritance)来构建 UI 能力,这一理念与 React 官方设计哲学一致,也使得组件边界清晰、易于测试和替换。
组合式组件(Composition Over Inheritance)
把大组件拆成"容器 + 子部件",由调用方自由组装:
interface CardProps { children: React.ReactNode variant?: 'default' | 'outlined' } export function Card({ children, variant = 'default' }: CardProps) { return <div className={`card card-${variant}`}>{children}</div> } export function CardHeader({ children }: { children: React.ReactNode }) { return <div className="card-header">{children}</div> } export function CardBody({ children }: { children: React.ReactNode }) { return <div className="card-body">{children}</div> } // Usage <Card> <CardHeader>Title</CardHeader> <CardBody>Content</CardBody> </Card>关键点:variant通过 props 提供,默认值为'default',调用方可通过variant="outlined"扩展视觉形态;children: React.ReactNode让内容完全由调用方控制。这种模式避免了深层次 props 透传和脆弱的类继承链。
复合组件(Compound Components):隐式共享状态
当多个子组件需要共享状态(如 Tab 的选中项)时,复合组件通过 Context 实现"父组件持有状态、子组件通过 Context 读写"的隐式契约:
interface TabsContextValue { activeTab: string setActiveTab: (tab: string) => void } const TabsContext = createContext<TabsContextValue | undefined>(undefined) export function Tabs({ children, defaultTab }: { children: React.ReactNode defaultTab: string }) { const [activeTab, setActiveTab] = useState(defaultTab) return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> {children} </TabsContext.Provider> ) } export function TabList({ children }: { children: React.ReactNode }) { return <div className="tab-list">{children}</div> } export function Tab({ id, children }: { id: string, children: React.ReactNode }) { const context = useContext(TabsContext) if (!context) throw new Error('Tab must be used within Tabs') return ( <button className={context.activeTab === id ? 'active' : ''} onClick={() => context.setActiveTab(id)} > {children} </button> ) } // Usage <Tabs defaultTab="overview"> <TabList> <Tab id="overview">Overview</Tab> <Tab id="details">Details</Tab> </TabList> </Tabs>这段代码有两个值得学习的防御性细节:Context 值声明为TabsContextValue | undefined,并在Tab中if (!context) throw new Error('Tab must be used within Tabs'),确保子组件误用(脱离Tabs渲染)时立即报错而非静默失败——这也是后面useMarkets等 Hooks 反复使用的同一模式。
Render Props:把渲染逻辑交给调用方
当组件负责数据获取等"行为",但希望完全由调用方决定"怎么渲染"时,render props 是最直接的方案:
interface DataLoaderProps<T> { url: string children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode } export function DataLoader<T>({ url, children }: DataLoaderProps<T>) { const [data, setData] = useState<T | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<Error | null>(null) useEffect(() => { fetch(url) .then(res => res.json()) .then(setData) .catch(setError) .finally(() => setLoading(false)) }, [url]) return <>{children(data, loading, error)}</> } // Usage <DataLoader<Market[]> url="/api/markets"> {(markets, loading, error) => { if (loading) return <Spinner /> if (error) return <Error error={error} /> return <MarketList markets={markets!} /> }} </DataLoader>注意泛型DataLoader<T>:类型参数随调用处的泛型实参(如Market[])推断,children 函数签名携带完整的类型信息,调用方在渲染分支里可以安全访问markets。缺点同样明显:回调嵌套层级变深后可读性下降,指南后文的useQueryHook 正是这一模式在 Hooks 形态下的演进版本。
自定义 Hooks 模式:复用状态逻辑
Hooks 是 React 16.8+ 复用"有状态逻辑"(而非 UI)的标准方式。指南给出了三个高频场景。
状态开关 Hook:useToggle
export function useToggle(initialValue = false): [boolean, () => void] { const [value, setValue] = useState(initialValue) const toggle = useCallback(() => { setValue(v => !v) }, []) return [value, toggle] } // Usage const [isOpen, toggleOpen] = useToggle()useCallback保证toggle引用稳定,作为 props 传给子组件时不会引发多余重渲染;返回元组(数组解构)与useState的使用体验保持一致。
异步数据获取 Hook:useQuery
这是渲染 props 的 Hooks 化版本,提供了data / error / loading / refetch四元返回和回调选项:
interface UseQueryOptions<T> { onSuccess?: (data: T) => void onError?: (error: Error) => void enabled?: boolean } export function useQuery<T>( key: string, fetcher: () => Promise<T>, options?: UseQueryOptions<T> ) { const [data, setData] = useState<T | null>(null) const [error, setError] = useState<Error | null>(null) const [loading, setLoading] = useState(false) const refetch = useCallback(async () => { setLoading(true) setError(null) try { const result = await fetcher() setData(result) options?.onSuccess?.(result) } catch (err) { const error = err as Error setError(error) options?.onError?.(error) } finally { setLoading(false) } }, [fetcher, options]) useEffect(() => { if (options?.enabled !== false) { refetch() } }, [key, refetch, options?.enabled]) return { data, error, loading, refetch } } // Usage const { data: markets, loading, error, refetch } = useQuery( 'markets', () => fetch('/api/markets').then(r => r.json()), { onSuccess: data => console.log('Fetched', data.length, 'markets'), onError: err => console.error('Failed:', err) } )要点拆解:
key目前作为 effect 的触发依赖([key, refetch, options?.enabled]),key 变化即重新拉取,这也是后续演进为 TanStack Query 这类缓存库的雏形;options?.enabled !== false支持"手动触发"场景(如等待用户点击后再请求);err as Error的类型断言处理了catch捕获值类型不确定的问题。
防抖 Hook:useDebounce
搜索输入框是高发场景——每次击键都发请求会打爆后端,防抖可以把请求推迟到用户停止输入之后:
export function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState<T>(value) useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value) }, delay) return () => clearTimeout(handler) }, [value, delay]) return debouncedValue } // Usage const [searchQuery, setSearchQuery] = useState('') const debouncedQuery = useDebounce(searchQuery, 500) useEffect(() => { if (debouncedQuery) { performSearch(debouncedQuery) } }, [debouncedQuery])原理:value或delay变化时,effect 先清理上一个定时器再新建,只有停止变化超过delay(此处 500ms)后debouncedValue才会更新,搜索请求随之后移。
状态管理模式:Context + Reducer
对中等规模的领域状态(如"市场列表 + 当前选中项 + 加载态"),指南推荐Context + useReducer:Reducer 集中管理状态转换,Context 负责向下分发,再通过自定义 Hook 收口使用方 API:
interface State { markets: Market[] selectedMarket: Market | null loading: boolean } type Action = | { type: 'SET_MARKETS'; payload: Market[] } | { type: 'SELECT_MARKET'; payload: Market } | { type: 'SET_LOADING'; payload: boolean } function reducer(state: State, action: Action): State { switch (action.type) { case 'SET_MARKETS': return { ...state, markets: action.payload } case 'SELECT_MARKET': return { ...state, selectedMarket: action.payload } case 'SET_LOADING': return { ...state, loading: action.payload } default: return state } } const MarketContext = createContext<{ state: State dispatch: Dispatch<Action> } | undefined>(undefined) export function MarketProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(reducer, { markets: [], selectedMarket: null, loading: false }) return ( <MarketContext.Provider value={{ state, dispatch }}> {children} </MarketContext.Provider> ) } export function useMarkets() { const context = useContext(MarketContext) if (!context) throw new Error('useMarkets must be used within MarketProvider') return context }值得注意的实践:
- Action 使用可辨识联合(discriminated union):每个 action 都有字面量
type,reducer 的switch能获得穷尽性检查; - 不可变更新:每个 case 都基于
...state展开后覆盖目标字段,避免原地修改; - Provider 边界错误显式化:
useMarkets在 Provider 缺失时抛错,与复合组件Tab的防御策略一致; - 状态分层:
useReducer适合跨组件共享的领域状态;局部 UI 状态(如开关、输入框值)仍应使用useState,避免过度设计。
性能优化:记忆化、代码分割与虚拟化
指南的性能章节针对"大数据列表 + 重组件"场景给出三层手段。
Memoization:useMemo / useCallback / React.memo
// ✅ useMemo for expensive computations const sortedMarkets = useMemo(() => { return markets.sort((a, b) => b.volume - a.volume) }, [markets]) // ✅ useCallback for functions passed to children const handleSearch = useCallback((query: string) => { setSearchQuery(query) }, []) // ✅ React.memo for pure components export const MarketCard = React.memo<MarketCardProps>(({ market }) => { return ( <div className="market-card"> <h3>{market.name}</h3> <p>{market.description}</p> </div> ) })三者分工明确:useMemo缓存昂贵计算结果(排序、过滤、格式化),useCallback稳定函数引用以配合子组件浅比较,React.memo让纯展示组件在 props 未变时跳过重渲染。三者必须配套使用才有效果——单独memo而父组件每次渲染都传新函数引用,浅比较依然失败。
代码分割与懒加载:lazy + Suspense
import { lazy, Suspense } from 'react' // ✅ Lazy load heavy components const HeavyChart = lazy(() => import('./HeavyChart')) const ThreeJsBackground = lazy(() => import('./ThreeJsBackground')) export function Dashboard() { return ( <div> <Suspense fallback={<ChartSkeleton />}> <HeavyChart data={data} /> </Suspense> <Suspense fallback={null}> <ThreeJsBackground /> </Suspense> </div> ) }lazy(() => import(...))把组件对应的 chunk 从主包中切出,首次渲染时才按需加载;Suspense提供加载期间的占位 UI。对图表、Three.js 背景等重型组件,这是降低首屏体积最直接的手段。fallback={null}适用于"加载完成前不显示任何内容也可接受"的装饰性组件。
长列表虚拟化:@tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual' export function VirtualMarketList({ markets }: { markets: Market[] }) { const parentRef = useRef<HTMLDivElement>(null) const virtualizer = useVirtualizer({ count: markets.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, // Estimated row height overscan: 5 // Extra items to render }) return ( <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }} > {virtualizer.getVirtualItems().map(virtualRow => ( <div key={virtualRow.index} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualRow.size}px`, transform: `translateY(${virtualRow.start}px)` }} > <MarketCard market={markets[virtualRow.index]} /> </div> ))} </div> </div> ) }虚拟化的本质:外层滚动容器高度固定(600px),内层撑起总高度getTotalSize(),但只绝对定位渲染视口附近的条目(配合overscan: 5提前渲染前后各 5 项作为缓冲),从而把 DOM 节点数从"万级"降到"几十级"。参数estimateSize是估算的行高(100px),getVirtualItems()返回带index / size / start的虚拟行元数据。
表单处理模式:受控组件 + 校验
指南给出的表单方案不依赖第三方库,用受控组件 + 校验函数即可覆盖大多数中轻量场景:
interface FormData { name: string description: string endDate: string } interface FormErrors { name?: string description?: string endDate?: string } export function CreateMarketForm() { const [formData, setFormData] = useState<FormData>({ name: '', description: '', endDate: '' }) const [errors, setErrors] = useState<FormErrors>({}) const validate = (): boolean => { const newErrors: FormErrors = {} if (!formData.name.trim()) { newErrors.name = 'Name is required' } else if (formData.name.length > 200) { newErrors.name = 'Name must be under 200 characters' } if (!formData.description.trim()) { newErrors.description = 'Description is required' } if (!formData.endDate) { newErrors.endDate = 'End date is required' } setErrors(newErrors) return Object.keys(newErrors).length === 0 } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!validate()) return try { await createMarket(formData) // Success handling } catch (error) { // Error handling } } return ( <form onSubmit={handleSubmit}> <input value={formData.name} onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="Market name" /> {errors.name && <span className="error">{errors.name}</span>} {/* Other fields */} <button type="submit">Create Market</button> </form> ) }要点:
- 受控组件:
value与onChange双向绑定,表单状态唯一来源是 React state; - 集中校验:
validate()收集所有字段错误到一个FormErrors对象,Object.keys(newErrors).length === 0决定是否放行提交; - 内联错误提示:错误通过
errors.name && <span className="error">就近渲染在对应字段旁; - 规则示例:必填校验(
!formData.name.trim())与长度上限(> 200)组合,体现"先必填后规则"的校验顺序。当表单规模变大时,可将校验抽象为 schema(如 zod / yup),但本模式为无额外依赖的起点。
错误边界模式:兜住渲染期异常
React 中 render 阶段抛出的错误会整树卸载,需要用类组件错误边界兜底。指南提供了完整实现:
interface ErrorBoundaryState { hasError: boolean error: Error | null } export class ErrorBoundary extends React.Component< { children: React.ReactNode }, ErrorBoundaryState > { state: ErrorBoundaryState = { hasError: false, error: null } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error } } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error('Error boundary caught:', error, errorInfo) } render() { if (this.state.hasError) { return ( <div className="error-fallback"> <h2>Something went wrong</h2> <p>{this.state.error?.message}</p> <button onClick={() => this.setState({ hasError: false })}> Try again </button> </div> ) } return this.props.children } } // Usage <ErrorBoundary> <App /> </ErrorBoundary>实现要点:
- 错误边界必须是类组件(Hooks 尚无等价能力),并实现
static getDerivedStateFromError以在渲染前同步更新 state; componentDidCatch负责副作用(打日志、上报监控);- 类属性
state声明初始值,避免构造函数样板; - "Try again" 按钮通过
setState({ hasError: false })重置边界,实现错误后的恢复尝试; - 边界应放置在粒度合适的位置(如页面级、路由级),而非仅仅包住整个 App,这样单模块崩溃不至于拖垮全站。
动画模式:Framer Motion 的进入/退出动效
指南使用framer-motion处理列表与弹窗的入场、出场动画,核心是motion组件 +AnimatePresence(后者负责监听子树卸载以播放 exit 动画):
import { motion, AnimatePresence } from 'framer-motion' // ✅ List animations export function AnimatedMarketList({ markets }: { markets: Market[] }) { return ( <AnimatePresence> {markets.map(market => ( <motion.div key={market.id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3 }} > <MarketCard market={market} /> </motion.div> ))} </AnimatePresence> ) } // ✅ Modal animations export function Modal({ isOpen, onClose, children }: ModalProps) { return ( <AnimatePresence> {isOpen && ( <> <motion.div className="modal-overlay" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} /> <motion.div className="modal-content" initial={{ opacity: 0, scale: 0.9, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.9, y: 20 }} > {children} </motion.div> </> )} </AnimatePresence> ) }要点:
- 列表动画:
initial定义初始态(透明 + 下移 20px),animate定义最终态,exit定义移除态,transition.duration: 0.3控制时长;每个子项必须有稳定的key供AnimatePresence追踪; - 弹窗动画:
isOpen && (...)条件渲染配合AnimatePresence实现开合两向动画;遮罩淡入淡出、内容缩放+位移动画分层呈现; - 动画参数(位移、透明度、时长)应统一收敛为设计令牌,避免各处硬编码。
无障碍模式:键盘导航与焦点管理
指南最后给出两个关键的 a11y 模式,保证交互组件对键盘与读屏器可用。
键盘导航:combobox 下拉
export function Dropdown({ options, onSelect }: DropdownProps) { const [isOpen, setIsOpen] = useState(false) const [activeIndex, setActiveIndex] = useState(0) const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault() setActiveIndex(i => Math.min(i + 1, options.length - 1)) break case 'ArrowUp': e.preventDefault() setActiveIndex(i => Math.max(i - 1, 0)) break case 'Enter': e.preventDefault() onSelect(options[activeIndex]) setIsOpen(false) break case 'Escape': setIsOpen(false) break } } return ( <div role="combobox" aria-expanded={isOpen} aria-haspopup="listbox" onKeyDown={handleKeyDown} > {/* Dropdown implementation */} </div> ) }role="combobox"+aria-expanded+aria-haspopup="listbox"告知读屏器这是一个展开的下拉组合框;ArrowDown/ArrowUp用Math.min/Math.max夹取索引边界,防止越界;Enter选择当前高亮项,Escape关闭,均调用e.preventDefault()避免浏览器默认行为(如表单回车提交)。
焦点管理:弹窗的焦点保存与恢复
export function Modal({ isOpen, onClose, children }: ModalProps) { const modalRef = useRef<HTMLDivElement>(null) const previousFocusRef = useRef<HTMLElement | null>(null) useEffect(() => { if (isOpen) { // Save currently focused element previousFocusRef.current = document.activeElement as HTMLElement // Focus modal modalRef.current?.focus() } else { // Restore focus when closing previousFocusRef.current?.focus() } }, [isOpen]) return isOpen ? ( <div ref={modalRef} role="dialog" aria-modal="true" tabIndex={-1} onKeyDown={e => e.key === 'Escape' && onClose()} > {children} </div> ) : null }- 打开时保存
document.activeElement到previousFocusRef,并把焦点移入弹窗容器(tabIndex={-1}使 div 可聚焦但不进入 Tab 序); - 关闭时把焦点还给触发元素,避免焦点丢失导致键盘用户"迷路";
role="dialog"+aria-modal="true"声明模态语义,Escape关闭是弹窗的通用快捷键约定。
总结与选型建议
cc-skill-frontend-patterns技能覆盖了 React/Next.js 前端开发中最常遇到的八类模式。概括其核心选型思路:
- 组件层:优先组合与复合组件,render props 适合"行为固定、渲染多变"的场景;
- 逻辑层:优先自定义 Hooks(
useToggle/useQuery/useDebounce),把状态逻辑收敛为可测试的单元; - 状态层:局部 UI 状态用
useState,跨组件领域状态用 Context + Reducer,规模再大再引入专门的状态库; - 性能层:
useMemo/useCallback/React.memo三件套处理重计算与引用稳定性,lazy + Suspense切包,虚拟化扛长列表; - 健壮性层:表单走受控 + 集中校验,渲染异常用错误边界兜底;
- 体验层:Framer Motion 负责动效,键盘导航与焦点管理保证无障碍底线。
详细指南末尾的提醒同样适用于读者:"现代前端模式的目标是构建可维护、高性能的用户界面——选择与项目复杂度相匹配的模式。" 这份技能文档在仓库中的完整代码与结构,可在 references/detailed-guide.md 中继续查阅,其目录条目与元数据见 data/catalog.json。按技能自身的约束,落地到具体环境时仍需结合实际项目进行验证、测试与评审。
- AI 技能
- AI 插件
【免费下载链接】agentic-awesome-skills
AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,445+ agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.
相关推荐
Agentic Awesome Skills 前端模式实战指南:从组件组合到性能优化的 React 开发范式
Agentic Awesome Skills 前端模式实战指南:从组件组合到性能优化的 React 开发范式 本文以 AAS(Agentic Awesome S
AI 技能AI 插件Agentic Awesome Skills 中的 antigravity-skill-orchestrator:基于记忆的智能技能编排 Meta-Skill 实战指南
Agentic Awesome Skills 中的 antigravity skill orchestrator:基于记忆的智能技能编排 Meta Skill
AI 技能AI 插件Agentic Awesome Skills 的 Frontend Patterns 规则模板:为 AGENTS.md 生成前端组件、状态管理与信任边界规则
Agentic Awesome Skills 的 Frontend Patterns 规则模板:为 AGENTS.md 生成前端组件、状态管理与信任边界规则 导
AI 技能AI 插件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考