ui-component-architect
Skill4 starsChanged 8 months agoMITView on GitHub
UI 组件架构专家 - React 组件设计、状态管理、性能优化、可访问性、视觉回归测试
In this repository14 other files it gives its agents.
- agent-architectSkill
- context-engineerSkill
- debug-diagnosticianSkill
- error-handling-specialistSkill
- harness-engineerSkill
- mcp-integration-specialistSkill
- performance-optimizerSkill
- prompt-engineerSkill
- security-compliance-officerSkill
- skill-developerSkill
- state-management-engineerSkill
- testing-quality-assurance-engineerSkill
- tool-integratorSkill
- verification-engineerSkill
The file
Files
UI Component Architect
使用此技能当你需要进行 React 组件架构设计、实现复杂 UI 状态管理、优化渲染性能、确保可访问性合规、或建立视觉回归测试流程。
目标
产出完整的 UI 组件架构方案,包括组件设计模式、状态管理策略、性能优化技术、可访问性指南和测试策略。
核心能力(基于 Claude Code源码)
1. React 组件设计模式
Compound Components Pattern
// 基于 Claude Code的 Message 组件系统
interface MessageContextValue {
messageId: string
role: 'user' | 'assistant'
isExpanded: boolean
}
const MessageContext = createContext<MessageContextValue | null>(null)
function Message({ children, messageId, role }: MessageProps) {
const [isExpanded, setIsExpanded] = useState(true)
const contextValue = useMemo(() => ({
messageId,
role,
isExpanded,
}), [messageId, role, isExpanded])
return (
<MessageContext.Provider value={contextValue}>
<div className="message" data-role={role}>
{children}
</div>
</MessageContext.Provider>
)
}
function MessageHeader() {
const context = useContext(MessageContext)
if (!context) throw new Error('MessageHeader must be used within Message')
return (
<header className="message-header">
<span>{context.role}</span>
<button onClick={() => {/* toggle expand */}}>
{context.isExpanded ? '▼' : '▶'}
</button>
</header>
)
}
function MessageContent({ children }: { children: ReactNode }) {
const context = useContext(MessageContext)
if (!context) throw new Error('MessageContent must be used within Message')
if (!context.isExpanded) return null
return (
<div className="message-content">
{children}
</div>
)
}
// 使用方式
<Message messageId="123" role="assistant">
<MessageHeader />
<MessageContent>
<Markdown content={messageContent} />
</MessageContent>
</Message>State Reducer Pattern
type MessageAction =
| { type: 'EXPAND' }
| { type: 'COLLAPSE' }
| { type: 'TOGGLE' }
| { type: 'SET_CONTENT'; content: string }
interface MessageState {
isExpanded: boolean
content: string
isLoading: boolean
}
function messageReducer(state: MessageState, action: MessageAction): MessageState {
switch (action.type) {
case 'EXPAND':
return { ...state, isExpanded: true }
case 'COLLAPSE':
return { ...state, isExpanded: false }
case 'TOGGLE':
return { ...state, isExpanded: !state.isExpanded }
case 'SET_CONTENT':
return { ...state, content: action.content }
default:
return state
}
}
function MessageWithReducer({ initialState }: MessageProps) {
const [state, dispatch] = useReducer(messageReducer, initialState)
return (
<MessageContext.Provider value={{ state, dispatch }}>
{/* children */}
</MessageContext.Provider>
)
}Control Props Pattern
interface AccordionProps {
expandedIndex?: number // controlled
defaultExpandedIndex?: number // uncontrolled
onChange?: (index: number) => void
children: ReactNode
}
function Accordion({
expandedIndex: controlledIndex,
defaultExpandedIndex = 0,
onChange,
children,
}: AccordionProps) {
const [uncontrolledIndex, setUncontrolledIndex] = useState(defaultExpandedIndex)
// Determine if controlled or uncontrolled
const isControlled = controlledIndex !== undefined
const expandedIndex = isControlled ? controlledIndex : uncontrolledIndex
const handleChange = useCallback((index: number) => {
if (!isControlled) {
setUncontrolledIndex(index)
}
onChange?.(index)
}, [isControlled, onChange])
return (
<AccordionContext.Provider value={{ expandedIndex, onChange: handleChange }}>
{children}
</AccordionContext.Provider>
)
}2. 高性能渲染优化
Virtual Scrolling
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualMessageList({ messages, containerRef }: MessageListProps) {
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => containerRef.current,
estimateSize: () => 100, // 估计每个项目高度
overscan: 5, // 预渲染前后各 5 个项目
})
const virtualItems = virtualizer.getVirtualItems()
return (
<div
ref={containerRef}
style={{
height: '600px',
overflow: 'auto',
}}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualItems.map((virtualRow) => (
<Message
key={messages[virtualRow.index]!.id}
message={messages[virtualRow.index]!}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
/>
))}
</div>
</div>
)
}Memoization Strategy
// Expensive computation memoization
const MemoizedMarkdown = memo(function Markdown({ content }: { content: string }) {
// Parse markdown (expensive)
const parsed = useMemo(() => parseMarkdown(content), [content])
// Syntax highlight code blocks (very expensive)
const highlighted = useMemo(() => {
return highlightCodeBlocks(parsed)
}, [parsed])
return <div dangerouslySetInnerHTML={{ __html: highlighted }} />
}, (prevProps, nextProps) => {
// Custom comparison
return prevProps.content === nextProps.content
})
// Event handler memoization
function MessageActions({ messageId, onArchive, onDelete }: ActionsProps) {
const handleArchive = useCallback(() => {
onArchive(messageId)
}, [messageId, onArchive])
const handleDelete = useCallback(() => {
onDelete(messageId)
}, [messageId, onDelete])
return (
<>
<button onClick={handleArchive}>Archive</button>
<button onClick={handleDelete}>Delete</button>
</>
)
}Windowing for Large Lists
function FixedSizeList({
height,
itemCount,
itemSize,
renderItem,
}: FixedSizeListProps) {
const [scrollTop, setScrollTop] = useState(0)
const visibleStart = Math.floor(scrollTop / itemSize)
const visibleEnd = Math.min(
itemCount,
visibleStart + Math.ceil(height / itemSize)
)
const items = []
for (let i = visibleStart; i < visibleEnd; i++) {
items.push(
<div
key={i}
style={{
position: 'absolute',
top: i * itemSize,
left: 0,
width: '100%',
height: itemSize,
}}
>
{renderItem(i)}
</div>
)
}
return (
<div
style={{ height, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ position: 'relative', height: itemCount * itemSize }}>
{items}
</div>
</div>
)
}3. 状态管理策略
Zustand for Global State
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
interface AppState {
// Session state
sessionId: string
turnCount: number
// UI state
sidebarOpen: boolean
theme: 'light' | 'dark'
// Actions
actions: {
setSidebarOpen: (open: boolean) => void
toggleTheme: () => void
incrementTurn: () => void
}
}
export const useAppStore = create<AppState>()(
subscribeWithSelector((set, get) => ({
// Initial state
sessionId: generateSessionId(),
turnCount: 0,
sidebarOpen: true,
theme: 'light',
// Actions
actions: {
setSidebarOpen: (open) => set({ sidebarOpen: open }),
toggleTheme: () => set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
})),
incrementTurn: () => set((state) => ({
turnCount: state.turnCount + 1
})),
},
}))
)
// Usage in components
function Sidebar() {
const { sidebarOpen, actions } = useAppStore()
return (
<aside className={sidebarOpen ? 'open' : 'closed'}>
<button onClick={() => actions.setSidebarOpen(!sidebarOpen)}>
Toggle
</button>
</aside>
)
}Jotai for Atomic State
import { atom, useAtom } from 'jotai'
// Atomic state
const messageIdsAtom = atom<string[]>([])
const messagesMapAtom = atom<Map<string, Message>>(new Map())
// Derived state
const messageCountAtom = atom(
(get) => get(messageIdsAtom).length
)
// Async state
const currentUserAtom = atom(async () => {
return fetchCurrentUser()
})
// Write-only atom
const addMessageAtom = atom(
null,
(get, set, message: Message) => {
set(messageIdsAtom, [...get(messageIdsAtom), message.id])
set(messagesMapAtom, new Map([
...get(messagesMapAtom),
[message.id]: message
]))
}
)
function MessageList() {
const [messageIds] = useAtom(messageIdsAtom)
const [messagesMap] = useAtom(messagesMapAtom)
const [, addMessage] = useAtom(addMessageAtom)
return (
<div>
{messageIds.map(id => (
<MessageCard key={id} message={messagesMap.get(id)!} />
))}
</div>
)
}4. 可访问性(A11y)实现
Semantic HTML & ARIA
interface DialogProps {
isOpen: boolean
onClose: () => void
title: string
children: ReactNode
}
function Dialog({ isOpen, onClose, title, children }: DialogProps) {
// Trap focus inside dialog
useEffect(() => {
if (!isOpen) return
const dialog = document.querySelector('[role="dialog"]')
const focusableElements = dialog?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const firstFocusable = focusableElements?.[0]
const lastFocusable = focusableElements?.[focusableElements.length - 1]
function handleTabKey(e: KeyboardEvent) {
if (e.key !== 'Tab') return
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault()
lastFocusable?.focus()
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault()
firstFocusable?.focus()
}
}
document.addEventListener('keydown', handleTabKey)
return () => document.removeEventListener('keydown', handleTabKey)
}, [isOpen])
if (!isOpen) return null
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
className="dialog-overlay"
onClick={(e) => e.target === e.currentTarget && onClose()}
>
<div className="dialog-content">
<h2 id="dialog-title">{title}</h2>
<button
aria-label="Close dialog"
onClick={onClose}
className="close-button"
>
×
</button>
{children}
</div>
</div>
)
}Keyboard Navigation
function Menu({ items, onSelect }: MenuProps) {
const [focusedIndex, setFocusedIndex] = useState(0)
function handleKeyDown(e: React.KeyboardEvent) {
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setFocusedIndex((prev) => (prev + 1) % items.length)
break
case 'ArrowUp':
e.preventDefault()
setFocusedIndex((prev) => (prev - 1 + items.length) % items.length)
break
case 'Enter':
case ' ':
e.preventDefault()
onSelect(items[focusedIndex])
break
case 'Escape':
onClose?.()
break
}
}
return (
<ul
role="menu"
onKeyDown={handleKeyDown}
>
{items.map((item, index) => (
<li
key={item.id}
role="menuitem"
tabIndex={index === focusedIndex ? 0 : -1}
aria-focused={index === focusedIndex}
className={index === focusedIndex ? 'focused' : ''}
onClick={() => onSelect(item)}
>
{item.label}
</li>
))}
</ul>
)
}5. 视觉回归测试
Percy Integration
import percySnapshot from '@percy/playwright'
import { test, expect } from '@playwright/test'
test.describe('Message Component', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/messages')
})
test('renders user message correctly', async ({ page }) => {
const message = page.getByTestId('message-user')
await expect(message).toBeVisible()
await percySnapshot(page, 'User Message - Default')
})
test('renders assistant message with markdown', async ({ page }) => {
const message = page.getByTestId('message-assistant')
await expect(message).toContainText('# Hello')
await percySnapshot(page, 'Assistant Message - Markdown')
})
test('handles long content gracefully', async ({ page }) => {
await page.evaluate(() => {
window.postMessage({
type: 'ADD_MESSAGE',
content: 'A'.repeat(10000)
})
})
await percySnapshot(page, 'Message - Long Content')
})
})Chromatic Integration
// .storybook/main.ts
module.exports = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@chromatic-com/storybook',
],
}
// Message.stories.tsx
export default {
component: Message,
title: 'Components/Message',
decorators: [
(Story) => (
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
<Story />
</div>
),
],
}
export const UserMessage = {
args: {
role: 'user',
content: 'Hello, world!',
},
}
export const AssistantMessageWithCode = {
args: {
role: 'assistant',
content: `Here's some code:
\`\`\`typescript
const greeting = 'Hello!'
console.log(greeting)
\`\`\``,
},
}
export const LoadingState = {
args: {
role: 'assistant',
isLoading: true,
},
}工作流程
第一步:组件设计 (20 分钟)
- 选择设计模式
- Compound Components: 用于复杂组合
- State Reducer: 用于灵活状态控制
- Control Props: 用于可控制的组件
- 定义接口
interface ComponentProps { // Required props children: ReactNode // Optional props className?: string // Controlled props value?: T defaultValue?: T onChange?: (value: T) => void }
第二步:实现优化 (40 分钟)
- 添加 Memoization
- 实现虚拟滚动
- 优化事件处理
- 添加键盘导航
第三步:测试验证 (20 分钟)
- 单元测试
- 视觉回归测试
- 可访问性测试
- 性能基准测试
规则
- ✅ 使用语义化 HTML
- ✅ 实现完整的键盘导航
- ✅ 添加适当的 ARIA 属性
- ✅ 使用 Memoization 优化渲染
- ✅ 实现错误边界
- ❌ 不要忽略可访问性
- ❌ 不要在渲染中进行昂贵计算
- ❌ 不要忘记清理副作用
- ❌ 不要直接修改 props
输出格式
📊 组件架构图
展示组件层次结构和数据流。
📝 实现代码
提供完整的组件实现,包括优化和测试。
🔍 测试报告
包含视觉回归测试结果和可访问性审计报告。
⚠️ 性能分析
| 指标 | 优化前 | 优化后 | 改进 |
| 首次渲染 | X ms | Y ms | -Z% |
| 重渲染次数 | X | Y | -Z% |
| FPS | X | Y | +Z% |
交付物清单
最终报告应包含:
- 组件设计文档
- 完整实现代码
- Storybook故事
- 视觉回归测试配置
- 可访问性审计报告
- 性能优化报告
- 使用文档
🤖 AI IDE 开发指南
本部分指导 AI IDE 按照 Claude Code的最佳实践开发 UI 组件系统。
AI IDE 开发前检查
设计系统验证
必须检查:
- Design Token定义完整
- 颜色系统一致
- 间距系统统一
- 字体系统清晰
警告信号:
- 硬编码的颜色值
- 不一致的间距
- 混用的单位 (px/rem/em)可访问性准备
必须存在:
- 键盘导航支持
- Focus管理
- ARIA标签
- 屏幕阅读器测试AI IDE 开发中指导
实时组件质量监控
当开发 UI 组件时,AI IDE应该:
- 设计模式检查
// ✅ 良好的复合组件模式 <Message> <Message.Header /> <Message.Content /> </Message> // ❌ 避免的模式 <Message showHeader={true} headerPosition="top" /> // AI IDE 应该:建议使用复合组件模式 - 性能优化检查
// ✅ 正确的memoization const ExpensiveComponent = memo(({ data }) => { const processed = useMemo(() => process(data), [data]) return <div>{processed}</div> }, (prev, next) => prev.data === next.data) // ❌ 避免的模式 function ExpensiveComponent({ data }) { const processed = process(data) // 每次都重新计算! return <div>{processed}</div> } // AI IDE 应该:建议添加 useMemo 和 memo - 可访问性检查
// ✅ 完整的键盘支持 function Menu({ items }) { const [focused, setFocused] = useState(0) return ( <ul role="menu" onKeyDown={handleKeyDown}> {items.map((item, i) => ( <li key={item.id} role="menuitem" tabIndex={i === focused ? 0 : -1} > {item.label} </li> ))} </ul> ) } // ❌ 缺少键盘支持 function Menu({ items }) { return ( <div> {items.map(item => <div onClick={select}>{item}</div>)} </div> ) } // AI IDE 应该:建议添加键盘导航和ARIA
视觉回归自动化
AI IDE应该提供:
- 自动截图: 在 PR 时自动运行 Percy/Chromatic
- 差异检测: 高亮显示视觉变化
- 基线管理: 轻松更新视觉基线
- 审批流程: 设计师审查视觉变化
AI IDE 完成后验证
UI 组件健康度评分卡
设计模式 (0-10分):
- 模式选择合适
- 组合性强
- 可扩展性好
性能 (0-10分):
- 渲染效率高
- 内存占用低
- 无内存泄漏
可访问性 (0-10分):
- WCAG 2.1 AA合规
- 键盘导航完整
- 屏幕阅读器友好
测试覆盖 (0-10分):
- 单元测试完整
- 视觉回归正常
- E2E测试通过
总分评级:
- 90-100: Excellent (生产就绪)
- 75-89: Good (少量优化)
- 60-74: Fair (需要改进)
- <60: Poor (不建议发布)自动化测试清单
AI IDE应该运行:
# 1. 单元测试
npm run test:unit -- components/
# 2. 视觉回归测试
npm run test:visual
# 3. 可访问性测试
npm run test:a11y
# 4. 性能测试
npm run test:performanceAI IDE 常见陷阱检测
🔴 高危问题(必须修复)
// ❌ 缺少错误边界
function App() {
return <ChildComponent /> // 子组件错误会导致整个应用崩溃
}
// AI IDE 应该:建议添加 ErrorBoundary
// ❌ 内存泄漏
useEffect(() => {
const subscription = eventBus.subscribe(handler)
// 没有返回清理函数
}, [])
// AI IDE 应该:提醒添加清理逻辑
// ❌ 直接修改props
function Child({ list }) {
list.push(newItem) // 直接修改props!
return <div>{list}</div>
}
// AI IDE 应该:立即警告并使用不可变更新🟡 中等风险(建议优化)
// ⚠️ 过度渲染
function Parent() {
const [count, setCount] = useState(0)
return <Child data={largeData} /> // count变化导致Child重渲染
}
// AI IDE 应该:建议拆分状态或使用memo
// ⚠️ 内联对象创建
function Component() {
return <Child style={{ margin: 10 }} /> // 每次渲染创建新对象
}
// AI IDE 应该:建议提取到组件外或使用useMemo🟢 低风险(可选改进)
// 💡 可以优化的模式
const className = isActive ? 'active' : 'inactive'
// AI IDE 可以建议:使用classnames库处理复杂条件AI IDE 代码审查检查清单
在 PR/MR阶段,AI IDE应该自动检查:
设计模式
- 选择了合适的组件模式
- 组合性优于配置
- 关注点分离清晰
- 单一职责原则
性能优化
- 使用React.memo优化
- useMemo/useCallback正确
- 虚拟滚动实现
- 无不必要的重渲染
可访问性
- 语义化HTML
- ARIA属性完整
- 键盘导航支持
- Focus管理正确
- 颜色对比度达标
测试
- 单元测试覆盖
- 视觉回归基线
- 可访问性测试
- 跨浏览器测试
AI IDE 集成实现示例
interface UIComponentRule {
id: string
description: string
severity: 'error' | 'warning' | 'info'
check: (componentFile: SourceFile) => Violation[]
fix?: (violation: Violation) => Fix
}
const rules: UIComponentRule[] = [
{
id: 'UI-001',
description: 'Interactive elements must have keyboard support',
severity: 'error',
check: (file) => {
const interactiveWithoutKeyboard = findInteractiveElementsWithoutKeyboard(file)
if (interactiveWithoutKeyboard.length > 0) {
return [{
message: 'Interactive element without keyboard support',
location: interactiveWithoutKeyboard.map(e => e.location)
}]
}
return []
},
fix: () => ({ type: 'add_keyboard_handler' })
},
{
id: 'UI-002',
description: 'Must use semantic HTML elements',
severity: 'error',
check: (file) => {
const nonSemanticDivs = findNonSemanticDivs(file)
if (nonSemanticDivs.length > 0) {
return [{
message: 'Consider using semantic HTML instead of div',
location: nonSemanticDivs.map(d => d.location)
}]
}
return []
}
},
{
id: 'UI-003',
description: 'Expensive computations should be memoized',
severity: 'warning',
check: (file) => {
const unmemoizedComputations = findUnmemoizedComputations(file)
if (unmemoizedComputations.length > 0) {
return [{
message: 'Expensive computation not memoized',
location: unmemoizedComputations.map(c => c.location)
}]
}
return []
},
fix: () => ({ type: 'wrap_with_usememo' })
},
{
id: 'UI-004',
description: 'Effects must clean up subscriptions',
severity: 'error',
check: (file) => {
const effectsWithoutCleanup = findEffectsWithoutCleanup(file)
if (effectsWithoutCleanup.length > 0) {
return [{
message: 'useEffect missing cleanup function',
location: effectsWithoutCleanup.map(e => e.location)
}]
}
return []
},
fix: () => ({ type: 'add_cleanup_return' })
},
{
id: 'UI-005',
description: 'Images must have alt text',
severity: 'error',
check: (file) => {
const imagesWithoutAlt = findImagesWithoutAlt(file)
if (imagesWithoutAlt.length > 0) {
return [{
message: 'Image missing alt attribute',
location: imagesWithoutAlt.map(img => img.location)
}]
}
return []
},
fix: () => ({ type: 'add_alt_attribute' })
}
// ... 更多规则
]AI IDE 学习路径
为了让 AI IDE更好地理解 UI 组件开发规范:
- 第一阶段: 学习 React 设计模式和最佳实践
- 第二阶段: 理解性能优化技术和应用场景
- 第三阶段: 掌握可访问性标准和实现方法
- 第四阶段: 实现视觉回归测试和工作流
- 第五阶段: 提供智能代码审查和优化建议
通过学习路径,AI IDE可以成长为能够独立设计和审查 UI 组件系统的专家。
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

