ECC frontend-patterns 技能详解:React 与 Next.js 组件、Hooks、性能与无障碍模式全景
ECC(Everything Claude Code)仓库中的 frontend-patterns 技能(.kiro/skills/frontend-patterns/SKILL.md)是一份面向 AI 编程助手的 React/Next.js 前端模式手册,覆盖组件组合、复合组件、Render Props、自定义 Hooks、Context + Reducer 状态管理、记忆化与虚拟列表性能优化、表单校验、Error Boundary、Framer Motion 动画与键盘无障碍等完整主题。读完本文,你将掌握这份技能中每个模式的可运行代码实现、关键陷阱(如 useQuery 的引用稳定性问题、sort 原地突变问题)的来源与规避方式,并了解该技能在 ECC 安装体系中的分发位置与调用方式。
一、技能定位:frontmatter 与 ECC 安装体系
该技能文件采用标准的 Kiro Skill 格式,YAML frontmatter 声明了技能的元信息:
---
name: frontend-patterns
description: >
Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
metadata:
origin: ECC
---
name 决定技能在 / 菜单中的调用名,description 用于让模型判断何时激活该技能,metadata.origin: ECC 标记其来源。仓库中还存在一份内容几乎完全一致的主副本 skills/frontend-patterns/SKILL.md,两者的差异仅在 description 多了一句激活条件(“Use when building or reviewing React or Next.js components, state, or render performance.”)。
从仓库的 manifests 可以确认该技能的模块化归属:
- 在 manifests/install-components.json 第 546 行附近,组件
skill:frontend-patterns被登记为framework-language模块下的一个 skill 组件,描述为 “React and frontend engineering patterns.”; - 在 manifests/install-modules.json 第 175 行附近,
skills/frontend-patterns被列为framework-language模块paths数组中的安装路径之一,该模块的defaultInstall为true,即默认随框架/语言类技能一起安装。
对于 Kiro 用户,ECC 提供了独立的 .kiro 集成目录。按照 .kiro/install.sh 的说明,安装命令为:
cd .kiro
./install.sh /path/to/your/project # 安装到指定项目
# 或 ./install.sh 安装到当前目录;./install.sh ~ 全局安装到 ~/.kiro/
该脚本对 agents skills steering hooks scripts settings 六个子目录执行非破坏性拷贝(目标已存在同名文件时跳过),其中第 66–77 行的技能拷贝逻辑是遍历 $SOURCE_KIRO/skills/*/ 每个技能目录并整体复制到 $TARGET/.kiro/skills/<skill_name>/。安装完成后,技能可在 Kiro 聊天中通过 / 菜单按需调用(.kiro/README.md 将其描述为 “React, Next.js, and frontend architecture patterns. Use when building UI components or optimizing frontend performance.”)。
二、激活场景(When to Activate)
技能文档开篇即列出了 7 类触发场景,这也是 AI 助手应当加载该技能判断的边界:
- 构建 React 组件(组合、props、渲染);
- 管理状态(useState、useReducer、Zustand、Context);
- 实现数据获取(SWR、React Query、Server Components);
- 性能优化(记忆化、虚拟列表、代码分割);
- 表单处理(校验、受控输入、Zod schema);
- 处理客户端路由与导航;
- 构建可访问、响应式的 UI 模式。
下文按文档骨架逐节展开每个主题的核心模式与实现要点。
三、组件模式(Component Patterns)
3.1 组合优于继承(Composition Over Inheritance)
文档首先给出的范式是用 children 插槽 + 独立子组件替代 props 堆叠或类继承,以 Card 组件族为例:
// 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>
要点在于:变体通过 variant?: 'default' | 'outlined' 这种联合类型枚举表达而非布尔开关,布局结构通过嵌套子组件表达而非 header、body、footer 等一堆具名 prop。这样组件 API 面保持封闭,扩展新区域只需新增子组件而不改动既有签名。
3.2 复合组件(Compound Components)
复合组件进一步把“共享状态”下沉到 Context,以 Tabs 为例。父组件 Tabs 持有 activeTab 状态并通过 TabsContext 下发,子组件 Tab 通过 useContext 读取,脱离父组件使用时抛出明确错误:
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,把“未包裹在 Provider 中”这一状态显式化;子组件在 if (!context) throw new Error(...) 处 fail fast,而不是让 undefined.activeTab 在深层渲染时才炸掉。这与后文 useMarkets hook 的守卫是同一种手法。
3.3 Render Props 模式
DataLoader 展示了用泛型 + 渲染函数把“取数逻辑”与“展示逻辑”解耦:
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>
从该实现的结构看,<T> 让调用侧在 JSX 中显式给出 Market[],三元组 (data, loading, error) 把三态语义固化成函数签名,调用方无需关心内部 state 结构。局限也很明显:fetch 未带 AbortController,url 变化时旧请求不会被取消,生产使用需自行补充(文档未处理这一细节)。
四、自定义 Hooks 模式(Custom Hooks Patterns)
4.1 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()
返回值采用 [value, action] 元组,toggle 通过 useCallback(..., []) + 函数式更新 setValue(v => !v) 保证引用稳定且永远基于最新值,不会因闭包捕获旧值而出错。
4.2 useQuery:引用稳定性与无限请求循环
这是文档中注释最密、信息量最大的一个 Hook。核心问题在源码注释中有完整交代:调用方常以内联函数与对象字面量传参,若直接把 fetcher/options 放进 useCallback/useEffect 的依赖,每次渲染都会产生新的 refetch 引用,effect 在下一次 state 更新后重跑,形成无限 fetch 循环。解法是把最新值持续写入 ref,让 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)
// 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)
}
)
逐点拆解:
fetcherRef/optionsRef由一个不带依赖数组的useEffect在每次渲染后同步最新值,因此refetch依赖列表可以为空,引用永久稳定;- 触发请求的 effect 依赖为
[key, enabled, refetch],即只有业务 key 变化或开关切换时才重新拉取,key是缓存/请求身份的唯一标识; enabled !== false的默认开启语义意味着省略options或省略enabled都会正常发请求;- 回调使用可选链
optionsRef.current?.onSuccess?.(result),任一缺省都不抛错; - 局限:该实现没有请求取消(并发下后发请求可能覆盖先发结果)、没有重试与 stale-while-revalidate,文档定位为轻量版 pattern 而非完整数据层。
4.3 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 中 clearTimeout(handler):每次 value 变化都会先清掉上一轮定时器再重新计时,从而把连续输入压缩为一次下游副作用。文档示例中“先防抖、再在独立 effect 里消费”的两段式写法,把“值稳定”与“发起搜索”两个关注点分离,if (debouncedQuery) 还顺带过滤了空串查询。
五、状态管理模式:Context + Reducer
对于多个组件共享、且变化路径可枚举的领域状态,文档给出的组合是 useReducer + Context + 命名守卫 hook,以市场(market)域为例:
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 联合类型把合法变更收敛为封闭集合,TypeScript 会在 reducer 与调用侧同时给出穷尽检查;
- Context value 同时暴露
state与dispatch,消费侧拿到的是完整 store 而非拆散的字段; - 命名 hook(
useMarkets)替代裸的useContext,既提供了文档中提到的 “must be used within ...Provider” 运行时守卫,也让调用点语义自解释; reducer初始 state 直接写在useReducer(reducer, initialState)第二参数,无 lazy init 需求。
文档在 “When to Activate” 中并列提及 Zustand——从该技能的结构推断,其立场是:域内共享且变更可枚举时优先 Context + Reducer 这套 React 原生方案,跨大型组件树的高频更新场景再引入 Zustand 之类的外部 store(文档正文未展开 Zustand 代码)。
六、性能优化(Performance Optimization)
6.1 记忆化(Memoization)
文档用三个 PASS 示例分别对应三种记忆化手段,并附带一个极易踩坑的注释:
// 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>
)
})
三条边界划分:
useMemo用于昂贵计算。特别地,Array.prototype.sort是原地突变操作,文档明确要求[...markets].sort(...)先拷贝,否则会直接改写 props/state 里的原始数组,破坏 React 的不可变数据流假设;useCallback用于“传给会被React.memo包装的子组件的函数”,它本身不省渲染,而是保证React.memo的浅比较能命中——6.3 的MarketCard正是这一配合的落点;React.memo用于纯展示组件,React.memo<MarketCardProps>的显式泛型写法让 props 类型在匿名箭头函数场景下依然可检查。
三者应视为组合拳而非独立选项:memo 的组件 + useCallback 的回调 + useMemo 的派生数据,缺任何一环都可能让另外两环失效。
6.2 代码分割与懒加载(Code Splitting & Lazy Loading)
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(...)) 让打包器对每个动态 import 产出独立 chunk,首次导航不加载图表与 3D 背景的 JS。文档展示了两种 fallback 策略:关键内容用 <ChartSkeleton /> 骨架屏占位保持布局稳定,装饰性的 ThreeJsBackground 用 fallback={null} 静默加载,避免无意义的闪烁。
6.3 长列表虚拟化(Virtualization)
基于 @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>
)
}
实现机制可以拆成四层:
count: markets.length声明数据总量;getScrollElement: () => parentRef.current把滚动事件源指向外层 600px 高的overflow: auto容器;estimateSize: () => 100是预估行高——在测量真实高度之前用于定位计算,overscan: 5则在可视区上下各多渲染 5 行,缓冲快速滚动时的空白;- 内层包裹 div 用
virtualizer.getTotalSize()撑起整个列表的“虚拟高度”,滚动条行为与真实长列表一致; - 每个可视行
position: absolute+transform: translateY(virtualRow.start)绝对定位到计算出的偏移,因此 DOM 中始终只有可视区附近的行节点,把 O(N) 节点数压成 O(1)。
6.4 表单处理(Form Handling Patterns)
受控表单 + 显式校验层,字段为市场创建场景(name / description / endDate):
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>
)
}
结构要点:errors 与 formData 是平行的两个 state,FormErrors 的字段全部可选(name? 等),错误是“存在即显示”而非“清空即删除”;validate 采用“先聚合全部错误再一次性 set”的策略,避免逐字段 setState 造成多次渲染;onChange 使用函数式更新 setFormData(prev => ...) 防止快速输入时闭包读到旧 state;e.preventDefault() 阻止原生提交后才进入校验与 createMarket 异步流程。frontmatter 激活场景提到 Zod,但从源码结构看,正文给出的是零依赖的手工校验版本,Zod 属于调用方可选的升级方向而非文档强制项。
七、错误边界(Error Boundary Pattern)
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>
该实现完整覆盖了错误边界的两个生命周期入口:static getDerivedStateFromError 负责在渲染阶段捕获错误并翻转 hasError 状态(这是显示 fallback 的必要条件),componentDidCatch 负责在提交阶段记录 errorInfo(包含组件堆栈)。Try again 按钮把 hasError 复位为 false 让子树重新渲染——注意这只是复位边界状态,若子组件错误是确定性的,重试仍会再次触发捕获。错误边界是文档正文中唯一必须使用 class 组件的模式,因为 hooks 尚无渲染错误捕获能力。
八、动画模式(Animation Patterns,Framer Motion)
8.1 列表进入/退出动画
AnimatePresence 包裹列表是 Framer Motion 实现退出动画的前提——子项卸载时它负责保留节点直到 exit 动画完成:
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>
)
}
三态 initial → animate → exit 分别对应挂载、稳定态、卸载;key={market.id} 是 AnimatePresence 追踪增删的依据,键不稳定会导致进出场动画丢失。
8.2 Modal 双图层动画
// 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>
)
}
遮罩层只做透明度淡入淡出,内容层叠加 scale: 0.9 → 1 与 y: 20 → 0 的位移,形成“面板浮出”的层次差异;两个 motion 节点同处一个 AnimatePresence 且由同一个 {isOpen && ...} 条件控制,保证退出时两层同步淡出而不是内容先消失。
九、无障碍模式(Accessibility Patterns)
9.1 键盘导航(Combobox 交互模式)
Dropdown 实现了一套完整的键盘协议,Aria 属性与按键处理一一对应:
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>
)
}
可复用的细节:ArrowDown/ArrowUp 用 Math.min / Math.max 钳制索引边界,配合 e.preventDefault() 阻止页面滚动等默认行为;Enter 确认选择并关闭,Escape 仅关闭不提交;role="combobox" + aria-expanded + aria-haspopup="listbox" 三件套让辅助技术能正确播报展开状态。文档此处只给到交互骨架({/* Dropdown implementation */} 留白),ARIA 完整实现(aria-activedescendant、选项 role="option" 等)需按 skills/frontend-a11y 等更细粒度的技能补全。
9.2 焦点管理(Focus Management)
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
}
要点:
document.activeElement在打开瞬间快照到previousFocusRef,关闭时.focus()精确回到原触发元素,而不是退回<body>;tabIndex={-1}让 dialog 根节点可被程序化聚焦却不进入 Tab 序;role="dialog"+aria-modal="true"告知辅助技术当前处于模态层,背景内容被隐式从可达性树排除;Escape关闭与 9.1 的 Dropdown 保持一致的关闭键约定。
该 Modal 与 8.2 的动画 Modal 是同一组件的两个切面:组合时把动画版的外层与焦点管理逻辑合并在同一个 ModalProps 契约下即可。
十、仓库内的配套技能与规则
frontend-patterns 在 ECC 技能体系中不是孤立的,从 skills/ 目录与 rules/ 目录的交叉引用看,可搭配使用的邻近技能包括:
- skills/react-patterns/SKILL.md:React 组件与 hooks 的更通用模式;
- skills/react-performance/SKILL.md:针对 React 渲染性能的专项优化;
- skills/frontend-a11y/SKILL.md 与 skills/accessibility/SKILL.md:无障碍实现细则,承接本文第九节留下的实现空白;
- skills/nextjs-turbopack/SKILL.md:Next.js 构建侧(对应文档 “When to Activate” 中的 Next.js 场景);
- rules/react/patterns.md:React 规则的规则文件形态,供不同 harness 加载。
Kiro 集成侧,该技能与 33 个 agents、22 个 steering 文件、13 个 hooks 同属 .kiro 安装包的 43 个技能之一,安装后通过 / 菜单按名调用;react-reviewer、react-build-resolver 等 Kiro agent(.kiro/README.md Agent 清单)在评审与排障 React/Next.js 代码时会引用同类模式,与本文第二、三节的组合/复合组件/错误边界写法互为印证。
十一、小结
.kiro/skills/frontend-patterns/SKILL.md 把前端工程约束组织成八组可直接复制的模式:组件层用组合、复合组件与 Render Props 控制 API 面;Hooks 层用 ref 同步技巧解决 useQuery 的引用稳定性问题,用 cleanup 定时器实现防抖;状态层用 Action 联合类型 + 命名守卫 hook 约束 Context 使用;性能层用“拷贝后排序”、lazy + Suspense 分层 fallback 与 TanStack Virtual 绝对定位布局覆盖三条主线;再加上受控表单校验、class 版 Error Boundary、AnimatePresence 双图层动画与 combobox/焦点恢复无障碍骨架。文档结尾给出的原则同样适用:这些模式面向“可维护、高性能的界面”,实际选型应匹配项目复杂度——轻量页面不必上虚拟化,简单弹窗也不必叠加焦点管理,但 sort 前拷贝、refetch 引用稳定、Provider 缺失守卫这几条是任何规模 React 项目的硬性纪律。
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