ECC 前端模式技能:React / Next.js 组件、状态、性能与无障碍的完整模式库
本文以 ECC 仓库中的前端模式技能文档 .agents/skills/frontend-patterns/SKILL.md 为主体,系统讲解 React / Next.js 前端的组件组合、自定义 Hook、状态管理、性能优化、表单处理、错误边界、动画与无障碍等核心模式。该文档是 ECC "agent harness" 体系中的一份技能(Skill)文件,供 Claude Code、Codex、Opencode、Cursor 等编码代理在构建或评审 React / Next.js 代码时自动激活。读完后,你将掌握一套可直接复制到项目中的前端模式代码,并理解每个模式背后的原理、适用边界,以及 ECC 仓库中配套的 React 规则集(rules/react/hooks.md、rules/react/patterns.md)如何与技能协同工作。
技能定位与激活条件
frontend-patterns 是一份带 YAML frontmatter 的技能定义,其元数据声明了名称与激活描述:
---
name: frontend-patterns
description: Frontend development patterns for React, Next.js, state management,
performance optimization, and UI best practices. Use when building or reviewing
React or Next.js components, state, or render performance.
---
从源码结构看,该技能通过 frontmatter 的 description 字段让代理判断"何时激活"。文档中列出的激活场景(When to Activate)包括:
- 构建 React 组件(组合、props、渲染)
- 管理状态(
useState、useReducer、Zustand、Context) - 实现数据获取(SWR、React Query、Server Components)
- 优化性能(记忆化、虚拟化、代码分割)
- 处理表单(校验、受控输入、Zod schema)
- 处理客户端路由与导航
- 构建可访问、响应式的 UI 模式
该技能在仓库中不止存在于 .agents/skills/ 下,还有一份镜像 skills/frontend-patterns/SKILL.md,并被安装清单 manifests/install-modules.json 收录("skills/frontend-patterns" 条目),用于按模块选择性安装到目标项目的代理配置中。此外,agents/typescript-reviewer.md 在评审 TS/JS 代码时也会显式引用 frontend-patterns 作为模式来源,说明它在 ECC 的代码评审链路中承担"前端模式事实标准"的角色。
隐私与数据边界
技能文档专设一节(Privacy and Data Boundaries)约束前端示例中的数据处理,要点包括:
- 前端示例应使用合成数据或领域通用数据;
- 除非用户明确要求并配套校验、脱敏与访问控制,否则不得收集、记录、持久化或展示凭据、访问令牌、SSN、健康数据、支付细节、私人邮箱、手机号等敏感个人数据;
- 未经明确批准,不要添加分析、追踪像素、第三方脚本或外部数据回传;
- 处理用户数据时优先最小权限 API、日志记录前的客户端脱敏,以及每个边界处的服务端校验。
这一节体现了 ECC 作为"安全优先"agent harness 的一贯取向:模式技能不仅回答"怎么写",还回答"哪些数据不能碰"。
组件模式
组合优于继承
React 没有组件继承模型,组合是构建复杂 UI 的基本手段。文档给出的 Card 系列组件演示了以 children 为核心的组合方式,variant 属性提供样式变体而不引入继承层次:
// PASS: GOOD: Component composition
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>
这与 skills/react-patterns/SKILL.md 中"Composition Over Inheritance"核心原则一致:用 children、render props 或组件 props 组合,而不是层层继承。
复合组件(Compound Components)
复合组件用 Context 把一组兄弟组件的状态"接线"起来。文档以 Tabs 为例:父组件 Tabs 持有 activeTab 状态并通过 TabsContext 下发,子组件 TabList 与 Tab 消费该上下文:
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 的默认值刻意设为 undefined 而非一个"看似正常"的对象,这样子组件脱离子树使用时会立刻抛出 Tab must be used within Tabs 错误,把配置错误暴露在开发期;其二,调用方 API 是声明式的(<Tabs defaultTab=...>),状态封装完全内聚,外部不需要知道 Context 的存在。
Render Props 模式
当组件需要把"内部状态"交给外部决定如何渲染时,用 render props 把渲染函数作为 prop 传入:
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> 是泛型组件,消费方在渲染函数里自行决定 loading / error / success 三态如何呈现,容器与表现彻底解耦。这与 rules/react/patterns.md 中 Container / Presentational 的划分相互呼应:数据获取留在容器(这里是 DataLoader),渲染决策交给表现层。
自定义 Hook 模式
状态管理 Hook:useToggle
最小的 Hook 抽取案例——把一个布尔状态与稳定的切换函数打包:
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()
两个要点:setValue(v => !v) 使用函数式更新,保证连续快速点击时始终基于最新值计算;useCallback 依赖数组为空,使 toggle 身份稳定,可以作为 prop 传给 React.memo 包裹的子组件而不破坏其记忆化。这与 rules/react/hooks.md 中"函数式更新 + 只在身份稳定有意义时才用 useCallback"的准则一致。
异步数据获取 Hook:useQuery
这是技能文档中最有分量的一段实现,它手工实现了一个最小可用的 query Hook,并特别处理了一个高频陷阱——无限请求循环:
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)
// Keep the latest fetcher/options in refs so refetch stays referentially
// stable even when callers pass inline functions and object literals.
// Without this, every render creates a new refetch, and the effect below
// re-runs after each state update - an infinite fetch loop.
const fetcherRef = useRef(fetcher)
const optionsRef = useRef(options)
useEffect(() => {
fetcherRef.current = fetcher
optionsRef.current = options
})
const refetch = useCallback(async () => {
setLoading(true)
setError(null)
try {
const result = await fetcherRef.current()
setData(result)
optionsRef.current?.onSuccess?.(result)
} catch (err) {
const error = err as Error
setError(error)
optionsRef.current?.onError?.(error)
} finally {
setLoading(false)
}
}, [])
const enabled = options?.enabled !== false
useEffect(() => {
if (enabled) {
refetch()
}
}, [key, enabled, refetch])
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)
}
)
原理剖析(结合 rules/react/hooks.md 的依赖数组与清理章节):
- 为什么需要
fetcherRef/optionsRef:调用方通常内联传入() => fetch(...)和{ onSuccess: ... },每次渲染都会生成新函数/新对象引用。如果把这些直接放进useCallback/useEffect的依赖数组,refetch每次渲染都会重建 → 触发 effect 重跑 → 状态更新引发再次渲染 → 又触发 effect,形成无限 fetch 循环。把最新值存入无依赖的useEffect更新的 ref,refetch的依赖数组才能保持为空、身份稳定。 enabled的默认语义:options?.enabled !== false意味着"未传即启用",只在显式enabled: false时跳过首次请求,适合作为条件查询的开关。- API 面:返回
{ data, error, loading, refetch }四元组,refetch可供手动刷新按钮复用。
生产环境中,这个手写 Hook 应被 SWR / TanStack Query 等服务器状态库替代(文档在 When to Activate 中也列出了这些库),但其 ref 技巧对理解任何"内联回调 + 稳定引用"场景(如事件订阅、WebSocket 回调)都通用。
防抖 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])
关键在于 cleanup:value 每次变化都会先清除上一个 timer 再开新 timer,实现"最后一次输入后 delay 毫秒才同步"。缺少 clearTimeout 的清理函数会在依赖快速变化时产生竞态。rules/react/hooks.md 对 useDebounce 的实现与清理要求("Every subscription, interval, listener... must clean up")与此完全一致,该 Hook 也是规则文档中"何时抽取自定义 Hook"(同一段逻辑出现在 2+ 组件、逻辑有可命名目的)的标准示例。
状态管理模式
Context + Reducer
当状态更新存在多种离散 action 且需要在多组件间共享时,useReducer + Context 是文档给出的组合方案(示例以"市场列表"域建模):
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
}
结构上遵循了与前文相同的防御式约定:Context 默认值 undefined + 自定义 Hook(useMarkets)中抛出带指引的错误信息。Action 使用判别联合(discriminated union)类型,payload 字段名统一为 payload,便于扩展新 action 时保持一致性。
需要强调的是配套规则文档 rules/react/patterns.md 中的"State Location Decision Tree"——Context 只适合低频读取、跨远端分支的场景(主题、鉴权、语言环境);高频更新应交给外部 store(Zustand / Jotai / Redux Toolkit),服务器派生数据应交给服务器状态库。"Most pages do not need context or a global store"(大多数页面既不需要 Context 也不需要全局 store),这是对技能示例的重要边界限定:上面的 Market 示例适用于"域级状态确实被多层共享"的场景,而非所有页面。
性能优化
记忆化:useMemo / useCallback / React.memo
// PASS: useMemo for expensive computations
// Copy before sorting - Array.prototype.sort mutates in place
const sortedMarkets = useMemo(() => {
return [...markets].sort((a, b) => b.volume - a.volume)
}, [markets])
// PASS: useCallback for functions passed to children
const handleSearch = useCallback((query: string) => {
setSearchQuery(query)
}, [])
// PASS: 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>
)
})
细节与依据:
[...markets].sort(...)先拷贝再排序,注释明确指出Array.prototype.sort是原地(in-place)修改——在渲染期原地修改 props 数组既破坏纯函数契约又会产生难以追踪的 bug;- 三层记忆化分工:
useMemo保护昂贵计算、useCallback保护传给 memo 子组件的函数身份、React.memo保护纯展示组件不随父组件无关状态重渲染。
但必须结合 rules/react/hooks.md 的立场理解本节:规则文档的默认立场是**"不要记忆化"**——只有在(1)值作为 prop 传给 React.memo 包裹的子组件且身份敏感、(2)值是其他 Hook 的依赖、(3)计算被 profile 证实昂贵这三种情况下才加 useMemo/useCallback。过早记忆化"adds noise, hides bugs, and can be slower than the recompute it replaces"(增加噪音、掩盖 bug、甚至比直接重算更慢)。技能示例展示的是"当确有必要时的正确写法",规则文档给出的是"何时才有必要"的判据,二者互补。
代码分割与懒加载
import { lazy, Suspense } from 'react'
// PASS: 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 提供加载中的 fallback。示例中两个 Suspense 边界各有侧重:图表用骨架屏 ChartSkeleton 占位,背景动画则 fallback={null}(不占位、直接淡入)。rules/react/patterns.md 补充了边界放置原则:Suspense 边界应贴近数据使用处而非路由根部,多个更窄的边界让已加载内容渐进呈现。
长列表虚拟化
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 的缓冲行,每行用 transform: translateY(...) 绝对定位到计算出的 start 偏移。estimateSize: () => 100 是行高估计值,行高固定时它就是精确值;行高不固定时 useVirtualizer 会在滚动过程中用实测尺寸校准。DOM 节点数从 markets.length 降为"视口行数 + 10"量级,这是万级列表保持 60fps 的关键手段。
表单处理模式
受控表单与校验
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>
)
}
要点:FormData/FormErrors 用可选字段精确建模"错误按字段出现";validate() 以"收集全部错误后一次性 setErrors"的方式实现,避免逐字段触发多次渲染;提交时用 e.preventDefault() 接管浏览器默认行为,校验失败早返回。rules/react/patterns.md 给出了受控/非受控的选择判据:当值驱动其他 UI、需要实时校验或格式化时用受控输入(本例);当表单有明确提交步骤时优先"非受控 + form action"(浏览器持有值,React 通过 FormData 读取);多步骤、动态字段数组、跨字段校验等复杂场景则交给 React Hook Form / TanStack Form 这类表单库。frontmatter 中也提到 Zod schema 作为校验工具链选项。
错误边界模式
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>
职责划分清晰:getDerivedStateFromError 负责"把错误写入状态、触发渲染 fallback"(渲染阶段),componentDidCatch 负责副作用(日志上报)——生产环境应把这里的 console.error 替换为真实监控上报。"Try again" 通过 setState({ hasError: false }) 重置边界,给子树一次重试机会。规则文档 rules/react/patterns.md 强调配套使用:每个 Suspense 边界上方都需要一个 Error Boundary(<ErrorBoundary><Suspense>...</Suspense></ErrorBoundary>),且错误边界目前必须是类组件(React 19 尚无函数式等价物),或使用 react-error-boundary 等库封装。
动画模式(Framer Motion)
列表动画
import { motion, AnimatePresence } from 'framer-motion'
// PASS: 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>
)
}
AnimatePresence 是关键:它让子元素在从树中移除时仍保留一帧以播放 exit 动画。新增项从下方 20px 淡入、移除项向上 20px 淡出,key={market.id} 保证进出场与数据项身份绑定。
模态框动画
// PASS: 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>
)
}
遮罩层与内容层分别定义动画:遮罩只做透明度渐变,内容层叠加缩放(0.9 → 1)与位移(20px → 0)产生"弹入"手感;isOpen && (...) 条件渲染配合 AnimatePresence,使 isOpen 变 false 时 exit 动画得以完整播放后再卸载。
无障碍模式
键盘导航(ARIA 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>
)
}
实现要点:方向键移动 activeIndex 并用 Math.min / Math.max 钳制在选项范围内(不会越界);Enter 选择当前项并关闭;Escape 关闭。ARIA 侧声明 role="combobox" + aria-expanded + aria-haspopup="listbox",让屏幕阅读器理解控件类型与展开状态。
焦点管理(Modal)
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
}
这是一套最小可用的焦点陷阱(focus trap):打开时记录 document.activeElement 并把焦点移到模态框(tabIndex={-1} 使 div 可编程聚焦但不进入 Tab 顺序),关闭时把焦点归还给触发元素——这对键盘与读屏用户体验至关重要。role="dialog" + aria-modal="true" 声明模态语义,Escape 键触发 onClose。注意它与前一节 Framer Motion 的 Modal 示例互补:一个解决"动起来",一个解决"访问得到"。
技能在 ECC 体系中的位置
从仓库结构看,这份技能与周边文件构成一个前端工程闭环:
- 技能层:.agents/skills/frontend-patterns/SKILL.md 提供模式库(本文主体),镜像安装副本为 skills/frontend-patterns/SKILL.md,由 manifests/install-modules.json 管理分发;
- 规则层:rules/react/hooks.md(Hooks 纪律:Rules of Hooks、依赖数组、清理、记忆化判据)、rules/react/patterns.md(容器/表现分离、状态位置决策树、RSC 边界、Suspense+ErrorBoundary 配对、表单策略)通过 frontmatter 的
paths字段匹配**/*.tsx、**/*.jsx等文件,在编辑对应文件时自动生效; - 技能互补:skills/react-patterns/SKILL.md 面向 React 18/19 深水区(RSC、form actions、状态决策树),skills/react-performance/SKILL.md 专注渲染性能;
- 命令层:commands/react-build.md 的
/react-build命令调用 react-build-resolver agent 修复构建失败,其"Related"部分显式把skills/frontend-patterns/列为配套技能。
使用时应结合的前提:技能文档中的示例假定 TypeScript + React 函数组件环境(部分示例隐含 React 18+,动画示例依赖 framer-motion,虚拟化示例依赖 @tanstack/react-virtual);示例数据域统一使用"markets(市场)"作为演示对象。若你的项目是 Next.js App Router,还应叠加规则文档中 Server/Client Component 边界与 "use client" 的约束。
结语
ECC 的 frontend-patterns 技能把"写前端时该遵循什么"沉淀为代理可直接消费的模式库:组件层给出组合、复合组件、render props 三种解耦手段;Hook 层给出 useToggle、useQuery(含防无限循环的 ref 技巧)、useDebounce 三个可复用单元;状态层给出 Context + Reducer 与状态位置决策树;性能层给出记忆化判据、lazy/Suspense 分割与 TanStack Virtual 虚拟化;表单、错误边界、Framer Motion 动画与键盘导航/焦点管理各成一体。配合 rules/react/ 下的规则文件,这些模式既可直接拷入项目使用,也可作为代码评审(如 /react-review、typescript-reviewer agent)的客观标尺——这正是 ECC "research-first" 开发范式在前端工程上的具体落点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00