
mcp-use 高级 Widget 开发指南从错误边界到虚拟化的复杂 MCP 交互界面实践【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit本篇技术指南围绕 CopilotKit 仓库中 mcp-apps-builder 技能 的Advanced Widget Patterns文档展开系统讲解在mcp-use框架下构建复杂、高性能 MCP Widget 的进阶技术错误边界、memoization、异步数据获取、复杂状态管理、大列表虚拟化、防抖搜索、无限滚动、本地存储持久化、拖放排序与键盘快捷键。读完本文你将掌握如何为 MCP 工具编写生产级的可视化交互界面并理解这些模式与mcp-use/react中useWidget、useCallTool、McpUseProvider等核心 API 的真实调用关系。文中所涉模式均可直接套用于仓库内的 open-mcp-client 示例如mcp-use-server的搜索 Widget。1. 前置知识Widget 生命周期与两条铁律高级模式建立在两个基础事实之上它们也是 basics.md 反复强调的核心Widget 在工具完成执行之前就会先渲染。首次渲染时isPending为trueprops是空对象{}直接访问props字段会抛错。因此任何 Widget 组件都必须在访问props之前检查isPending。根组件必须用McpUseProvider autoSize包裹它提供上下文并负责 iframe 尺寸自适应每个 return 分支包括 Loading 分支都不能遗漏。当 UI 需要调用 MCP 工具时统一使用useCallTool()hook。它提供类似 TanStack Query 的状态机其返回的状态标志见 interactivity.md贯穿本文所有示例Property说明isPending工具正在执行isSuccess已成功 ——data可用isError已失败 ——error可用isIdle尚未发起调用callTool即发即忘式调用可传onSuccess/onError/onSettled回调callToolAsync返回PromiseCallToolResult可用于await与串联调用在仓库的 mcp-use-server 示例 中可以看到这一模式的落地get-fruit-details是一个独立的数据工具由 Widget 内部通过useCallTool(get-fruit-details)触发而search-tools则返回widget({ props })驱动界面渲染。工具与 Widget 一一配对、职责分离是mcp-use的核心组织方式。2. 错误边界Error Boundaries为 Widget 兜底React 组件内部的运行时错误如后端返回了意外数据结构如果不加处理会直接导致整个 iframe 白屏。用 class 组件实现ErrorBoundary捕获渲染错误并展示可恢复的 fallback UIimport { Component, ReactNode } from react; import { McpUseProvider, useWidget } from mcp-use/react; interface ErrorBoundaryProps { children: ReactNode; } interface ErrorBoundaryState { hasError: boolean; error: Error | null; } class ErrorBoundary extends ComponentErrorBoundaryProps, ErrorBoundaryState { constructor(props: ErrorBoundaryProps) { super(props); this.state { hasError: false, error: null }; } static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: any) { console.error(Widget error:, error, errorInfo); } render() { if (this.state.hasError) { return ( div style{{ padding: 20, color: #c62828 }} h3Something went wrong/h3 p{this.state.error?.message}/p button onClick{() this.setState({ hasError: false, error: null })} Try Again /button /div ); } return this.props.children; } } // Usage export default function SafeWidget() { const { props, isPending } useWidget(); if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } return ( McpUseProvider autoSize ErrorBoundary WidgetContent props{props} / /ErrorBoundary /McpUseProvider ); }要点解读getDerivedStateFromError在渲染阶段捕获错误并更新状态componentDidCatch只用于记录错误日志如上报到监控系统不应在此处修改 state。Try Again 按钮通过setState({ hasError: false })将边界复位触发子树重新渲染。把ErrorBoundary放在McpUseProvider内部让 fallback UI 仍处于 Provider 上下文之中保证样式与 iframe 尺寸逻辑一致。3. 用 useMemo 缓存昂贵计算Widget 每次 props 变化都会重渲染。对排序、过滤、聚合这类计算量大的逻辑用useMemo限定其只在依赖项变化时执行。示例代码还展示了如何对isPending期间props未定义的情况做防御import { useMemo } from react; import { McpUseProvider, useWidget } from mcp-use/react; export default function OptimizedWidget() { const { props, isPending } useWidget(); // Expensive computation - only runs when props.items changes // Guard against isPending where props.items is undefined const sortedAndFiltered useMemo(() { if (!props.items) return { items: [], total: 0, avgScore: 0 }; let result props.items; // Filter result result.filter((item) item.active); // Sort result.sort((a, b) b.score - a.score); // Compute stats return { items: result, total: result.length, avgScore: result.length 0 ? result.reduce((sum, item) sum item.score, 0) / result.length : 0, }; }, [props.items]); if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } return ( McpUseProvider autoSize div style{{ padding: 20 }} pTotal: {sortedAndFiltered.total}/p pAverage: {sortedAndFiltered.avgScore.toFixed(2)}/p {sortedAndFiltered.items.map((item) ( div key{item.id}{item.name}/div ))} /div /McpUseProvider ); }值得注意的细节useMemo回调内部先做if (!props.items)空值守卫配合外层isPending早退形成双保险——useMemo是在渲染阶段同步执行的任何抛错都会直接中断渲染。依赖数组写[props.items]而非[props]避免其他无关 props 字段变化引发无谓重算。sort会原地修改数组示例中props.items来自工具返回的 props对 props 直接sort有修改上游数据的隐患在真实项目中建议先[...result]浅拷贝再排序见下文 5.4 节 useReducer 示例中的做法。4. useCallback让子组件免于无谓重渲染把回调函数传给子组件时如果每次父组件渲染都创建新函数引用即使子组件用React.memo包裹也无法命中缓存。useCallback配合React.memo可以同时稳定函数引用、锁定子组件渲染import { useCallback, useState } from react; import { McpUseProvider, useWidget, useCallTool } from mcp-use/react; export default function CallbackWidget() { const { props, isPending } useWidget(); const { callToolAsync } useCallTool(process-item); const [loadingId, setLoadingId] useStatestring | null(null); // Stable function reference const handleAction useCallback( async (id: string) { setLoadingId(id); try { await callToolAsync({ id }); } finally { setLoadingId(null); } }, [callToolAsync], ); if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } return ( McpUseProvider autoSize div {props.items.map((item) ( ItemRow key{item.id} item{item} onAction{handleAction} loading{loadingId item.id} / ))} /div /McpUseProvider ); } // Child component wont re-render unnecessarily const ItemRow React.memo(({ item, onAction, loading }: any) ( div span{item.name}/span button onClick{() onAction(item.id)} disabled{loading} {loading ? Processing... : Process} /button /div ));实现原理与注意点callToolAsync由useCallTool返回通常引用稳定因此依赖数组[callToolAsync]可行若你观察到回调仍频繁变化可检查useCallTool返回值是否被外层组件用内联对象包裹。共享同一个 hook 实例时每个条目独立的加载态必须靠loadingId显式追踪与 interactivity.md 中Per-Item Loading States一节完全一致。该模式与上节useMemo协同useCallback(fn, deps)本质是缓存函数引用的 useMemo二者常成对出现。5. 复杂状态管理useReducer 统一多维度 UI 状态当 Widget 同时存在多选、多条件筛选、排序字段、排序方向等多个交互状态时用useReducer把状态 变更动作收敛到一个纯函数里避免大量散落的useState和回调import { useReducer } from react; import { McpUseProvider, useWidget } from mcp-use/react; type State { selectedIds: Setstring; filters: { category: string; search: string }; sortBy: string; sortOrder: asc | desc; }; type Action | { type: TOGGLE_SELECT; id: string } | { type: SET_FILTER; key: string; value: string } | { type: SET_SORT; by: string } | { type: TOGGLE_SORT_ORDER } | { type: RESET }; function reducer(state: State, action: Action): State { switch (action.type) { case TOGGLE_SELECT: const newSelection new Set(state.selectedIds); if (newSelection.has(action.id)) { newSelection.delete(action.id); } else { newSelection.add(action.id); } return { ...state, selectedIds: newSelection }; case SET_FILTER: return { ...state, filters: { ...state.filters, [action.key]: action.value }, }; case SET_SORT: return { ...state, sortBy: action.by }; case TOGGLE_SORT_ORDER: return { ...state, sortOrder: state.sortOrder asc ? desc : asc, }; case RESET: return { selectedIds: new Set(), filters: { category: all, search: }, sortBy: name, sortOrder: asc, }; default: return state; } } export default function ComplexWidget() { const { props, isPending } useWidget(); const [state, dispatch] useReducer(reducer, { selectedIds: new Set(), filters: { category: all, search: }, sortBy: name, sortOrder: asc, }); if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } return ( McpUseProvider autoSize div style{{ padding: 20 }} input typetext value{state.filters.search} onChange{(e) dispatch({ type: SET_FILTER, key: search, value: e.target.value, }) } placeholderSearch... / button onClick{() dispatch({ type: RESET })} Reset Filters /button {/* ... render items with state */} /div /McpUseProvider ); }模式拆解用可辨识联合discriminated union定义Action每个分支对应一个不可变状态更新Set的更新必须新建实例再增删保证引用变化可被 React 识别。RESET动作让一键恢复默认筛选变成一次dispatch无需在组件里维护多份初始值常量。这一节与 state.md 的核心理念一致UI 状态选中项、筛选、排序、分页只活在 Widget 内部服务端状态数据列表、用户信息、API 结果才由工具返回。切勿为选中某项设置筛选这类纯 UI 行为去创建 MCP 工具——这也是 SKILL.md 中Widgets Own Their State黄金法则的要求。6. 大列表虚拟化只渲染可见行Widget 以 iframe 渲染动辄数千行的列表若全部挂载会拖垮性能。虚拟化通过视口高度内只渲染可见项 绝对定位占位实现import { useState, useRef, useEffect } from react; import { McpUseProvider, useWidget } from mcp-use/react; export default function VirtualizedList() { const { props, isPending } useWidget(); const [scrollTop, setScrollTop] useState(0); const containerRef useRefHTMLDivElement(null); const itemHeight 50; const containerHeight 400; const overscan 3; if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } const visibleStart Math.max( 0, Math.floor(scrollTop / itemHeight) - overscan, ); const visibleEnd Math.min( props.items.length, Math.ceil((scrollTop containerHeight) / itemHeight) overscan, ); const visibleItems props.items.slice(visibleStart, visibleEnd); return ( McpUseProvider autoSize div ref{containerRef} onScroll{(e) setScrollTop(e.currentTarget.scrollTop)} style{{ height: containerHeight, overflow: auto, position: relative, }} div style{{ height: props.items.length * itemHeight, position: relative, }} {visibleItems.map((item, index) ( div key{item.id} style{{ position: absolute, top: (visibleStart index) * itemHeight, height: itemHeight, width: 100%, padding: 12, borderBottom: 1px solid #eee, }} {item.name} /div ))} /div /div /McpUseProvider ); }实现机制外层滚动容器高度固定containerHeight: 400并开启overflow: auto内层撑起总高度items.length * itemHeight形成完整滚动条。依据scrollTop算出可见区间[visibleStart, visibleEnd)overscan此处为 3额外多渲染视口上下几行减少快速滚动时的空白闪烁。每个可见行用position: absolutetop精确定位到滚动内容中的对应位置。性能上滚动事件驱动setScrollTop会频繁重渲染属于教学级实现线上可进一步用requestAnimationFrame节流或直接引入成熟的react-virtual/tanstack/react-virtual等虚拟化库。7. 客户端异步数据获取直接 fetch 还是 useCallToolWidget 可以自行向后端 API 发起 fetch实现工具返回骨架数据、Widget 按需补全细节import { useState, useEffect } from react; import { McpUseProvider, useWidget } from mcp-use/react; export default function AsyncWidget() { const { props, isPending } useWidget(); const [details, setDetails] useStateRecordstring, unknown | null(null); const [loading, setLoading] useState(false); useEffect(() { if (!isPending props.itemId) { setLoading(true); fetch(/api/items/${props.itemId}/details) .then((res) res.json()) .then((data) setDetails(data)) .catch((err) console.error(Failed to load details:, err)) .finally(() setLoading(false)); } }, [isPending, props.itemId]); if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } return ( McpUseProvider autoSize div style{{ padding: 20 }} h2{props.title}/h2 {loading pLoading details.../p} {details ( div h3Details/h3 pre{JSON.stringify(details, null, 2)}/pre /div )} /div /McpUseProvider ); }但原文档给出了明确的取舍建议优先用工具调用而不是直接 API 调用。// ✅ Better - Use useCallTool const { callToolAsync } useCallTool(get-item-details); useEffect(() { if (!isPending props.itemId) { setLoading(true); callToolAsync({ id: props.itemId }) .then((result) setDetails(result)) .finally(() setLoading(false)); } }, [isPending, props.itemId, callToolAsync]);原因与仓库实践吻合工具调用经过 MCP 通道天然携带鉴权ctx.auth、输入校验Zod schema、可观测性与错误处理而裸fetch需要自己在 Widget 里重复实现这些边界。这与 SKILL.md 的Validate at Boundaries Only原则一脉相承。在 product-search.ts 中可以看到真实配对search-tools负责渲染结果列表get-fruit-details作为配套数据工具由 Widget 通过useCallTool按需调用服务端用outputSchema声明结构化返回。原文档同时提醒useEffect依赖数组要包含callToolAsync避免闭包捕获过期引用取消/竞态处理如请求序号比对或AbortController在复杂场景下仍需自行补充。8. 防抖搜索300ms 延迟避免风暴式调用搜索框每敲一个字符就触发一次工具调用会产生大量无效请求。用自定义useDebouncehook 把输入值延迟 300ms 后才提交import { useState, useEffect } from react; import { McpUseProvider, useWidget, useCallTool } from mcp-use/react; function useDebounceT(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] useStateT(value); useEffect(() { const handler setTimeout(() { setDebouncedValue(value); }, delay); return () { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } export default function DebouncedSearchWidget() { const { props, isPending } useWidget(); const { callToolAsync } useCallTool(search); const [search, setSearch] useState(); const [results, setResults] useState{ id: string; name: string }[]([]); const [searching, setSearching] useState(false); const debouncedSearch useDebounce(search, 300); useEffect(() { if (!debouncedSearch.trim()) { setResults([]); return; } setSearching(true); callToolAsync({ query: debouncedSearch }) .then((result) setResults(result.structuredContent?.items || [])) .catch((err) console.error(Search failed:, err)) .finally(() setSearching(false)); }, [debouncedSearch, callToolAsync]); if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } return ( McpUseProvider autoSize div style{{ padding: 20 }} input typetext value{search} onChange{(e) setSearch(e.target.value)} placeholderSearch... style{{ width: 100%, padding: 8 }} / {searching pSearching.../p} div {results.map((item) ( div key{item.id}{item.name}/div ))} /div /div /McpUseProvider ); }工作原理useDebounce通过useEffect设置定时器依赖value变化即重置计时只有连续 300ms 无输入debouncedSearch才更新为最新值。搜索副作用以debouncedSearch为触发源因此每次击键只会取消上一个定时器而不会立刻发请求trim()空值直接清空结果避免无效调用。返回结果从result.structuredContent?.items提取——这是mcp-use工具调用返回的结构化内容字段与 interactivity.md 中对CallToolResult的说明一致。9. 无限滚动IntersectionObserver 分页加载列表数据量大、有明确分页语义时无限滚动比一次性返回全部数据更友好。用IntersectionObserver监听列表底部的哨兵元素进入视口即加载下一页import { useState, useRef, useEffect } from react; import { McpUseProvider, useWidget, useCallTool } from mcp-use/react; interface Item { id: string; name: string; } export default function InfiniteScrollWidget() { const { props, isPending } useWidget{ items: Item[] }(); const { callToolAsync } useCallTool(load-more); const [items, setItems] useStateItem[]([]); const [loading, setLoading] useState(false); const [hasMore, setHasMore] useState(true); const observerTarget useRefHTMLDivElement(null); // Sync initial items from props once loaded useEffect(() { if (!isPending props.items) { setItems(props.items); } }, [isPending, props.items]); useEffect(() { const observer new IntersectionObserver( (entries) { if (entries[0].isIntersecting hasMore !loading) { loadMore(); } }, { threshold: 1.0 }, ); if (observerTarget.current) { observer.observe(observerTarget.current); } return () observer.disconnect(); }, [hasMore, loading]); const loadMore async () { setLoading(true); try { const result await callToolAsync({ offset: items.length, limit: 20, }); const newItems result.structuredContent?.items || []; if (newItems.length 0) { setHasMore(false); } else { setItems((prev) [...prev, ...newItems]); } } catch (error) { console.error(Failed to load more:, error); } finally { setLoading(false); } }; if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } return ( McpUseProvider autoSize div style{{ padding: 20 }} {items.map((item) ( div key{item.id} style{{ padding: 12, borderBottom: 1px solid #eee }} {item.name} /div ))} div ref{observerTarget} style{{ height: 20 }} {loading pLoading more.../p} {!hasMore pNo more items/p} /div /div /McpUseProvider ); }关键实现点props 同步与本地合并首屏数据来自 props随后由本地items状态接管增量追加。useEffect只在props.items就绪时同步一次后续全部依赖loadMore累积。加载锁与结束条件loading与hasMore双守卫防止并发触发工具返回空数组即置hasMore false哨兵区显示 No more items。分页参数offset: items.length, limit: 20把当前已加载数量作为下一页起点由配套服务端工具load-more按此契约返回切片。10. 本地存储持久化跨会话记住用户偏好把收藏最近查看主题偏好等轻量状态写入localStorage用户刷新或下次会话打开时自动恢复。自定义useLocalStoragehook 对读写都做 try/catch隐私模式、存储满、配额受限时降级为内存态import { useState, useEffect } from react; import { McpUseProvider, useWidget } from mcp-use/react; function useLocalStorageT( key: string, initialValue: T, ): [T, (value: T) void] { const [storedValue, setStoredValue] useStateT(() { try { const item window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(Error reading from localStorage:, error); return initialValue; } }); const setValue (value: T) { try { setStoredValue(value); window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error(Error writing to localStorage:, error); } }; return [storedValue, setValue]; } export default function PersistentWidget() { const { props, isPending } useWidget(); const [favorites, setFavorites] useLocalStoragestring[](favorites, []); const toggleFavorite (id: string) { setFavorites((prev) prev.includes(id) ? prev.filter((fav) fav ! id) : [...prev, id], ); }; if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } return ( McpUseProvider autoSize div {props.items.map((item) ( div key{item.id} button onClick{() toggleFavorite(item.id)} {favorites.includes(item.id) ? ⭐ : ☆} /button {item.name} /div ))} /div /McpUseProvider ); }使用建议惰性初始化useState(() ...)保证只在挂载时读一次存储JSON.parse失败被捕获并回退到initialValue。需要跨标签页同步时可补充window.addEventListener(storage, ...)监听原文档未展开属进阶扩展。注意权衡只持久化用户偏好类 UI 状态服务端权威数据仍应由工具返回避免本地存储与后端状态失同步——这再次呼应Widgets Own Their State的边界划分。11. 拖放排序原生 HTML5 DnD 实现列表重排不依赖第三方库用 HTML5draggableonDragStart/onDragOver/onDragEnd实现纯前端排序import { useState, useEffect } from react; import { McpUseProvider, useWidget } from mcp-use/react; interface Item { id: string; name: string; } export default function DraggableList() { const { props, isPending } useWidget{ items: Item[] }(); const [items, setItems] useStateItem[]([]); const [draggedIndex, setDraggedIndex] useStatenumber | null(null); // Sync items from props once loaded useEffect(() { if (!isPending props.items) { setItems(props.items); } }, [isPending, props.items]); const handleDragStart (index: number) { setDraggedIndex(index); }; const handleDragOver (e: React.DragEvent, index: number) { e.preventDefault(); if (draggedIndex null || draggedIndex index) return; const newItems [...items]; const draggedItem newItems[draggedIndex]; newItems.splice(draggedIndex, 1); newItems.splice(index, 0, draggedItem); setItems(newItems); setDraggedIndex(index); }; const handleDragEnd () { setDraggedIndex(null); // Optionally save new order with useCallTool }; if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } return ( McpUseProvider autoSize div {items.map((item, index) ( div key{item.id} draggable onDragStart{() handleDragStart(index)} onDragOver{(e) handleDragOver(e, index)} onDragEnd{handleDragEnd} style{{ padding: 12, margin: 4px 0, backgroundColor: draggedIndex index ? #e3f2fd : white, border: 1px solid #ddd, cursor: move, }} ⋮⋮ {item.name} /div ))} /div /McpUseProvider ); }实现要点draggedIndex记录当前被拖拽项onDragOver必须e.preventDefault()才允许放置随后用splice将拖拽项移动到目标索引并同步更新draggedIndex以支持连续换位。handleDragEnd里预留了用useCallTool持久化新顺序的扩展点——排序结果若要写回服务端应在此处触发工具调用而非在拖拽过程中频繁写入。该模式适合条目较少的排序场景超长列表建议与第 6 节的虚拟化或专门的 DnD 库结合。12. 键盘快捷键提升专业用户效率为 Widget 注册全局键盘事件覆盖保存、取消、方向导航等高频操作。注意useEffect的清理函数必须移除监听器避免重复挂载导致事件累积import { useEffect } from react; import { McpUseProvider, useWidget, useCallTool } from mcp-use/react; export default function KeyboardWidget() { const { props, isPending } useWidget(); const { callTool: save } useCallTool(save); useEffect(() { const handleKeyDown (e: KeyboardEvent) { // CtrlS to save if (e.ctrlKey e.key s) { e.preventDefault(); save({}); } // Escape to cancel if (e.key Escape) { // Handle escape } // Arrow keys for navigation if (e.key ArrowDown) { // Navigate down } }; window.addEventListener(keydown, handleKeyDown); return () window.removeEventListener(keydown, handleKeyDown); }, [save]); if (isPending) { return ( McpUseProvider autoSize divLoading.../div /McpUseProvider ); } return ( McpUseProvider autoSize div pKeyboard shortcuts:/p ul li kbdCtrlS/kbd - Save /li li kbdEsc/kbd - Cancel /li li kbd↑/↓/kbd - Navigate /li /ul /div /McpUseProvider ); }规范提示对CtrlS这类浏览器保留快捷键调用e.preventDefault()避免触发默认的保存网页行为。依赖数组[save]把useCallTool返回的callTool作为依赖保证回调引用最新。Widget 运行在 iframe 中window指向 iframe 自身的 window如需响应宿主页面按键需要额外的事件桥接原文档未展开按需自行实现。13. 最佳实践清单综合本技能文档与 SKILL.md 中Common Mistakes与Golden Rules两节高级 Widget 开发应遵循使用错误边界—— 渲染错误必须被优雅捕获不能白屏。用useMemo缓存昂贵计算—— 排序、过滤、聚合只在依赖变化时执行。输入防抖—— 搜索、筛选等高频输入必须延迟触发避免过量 API 调用。大列表虚拟化或分页—— 只渲染可见项。必要处持久化状态—— 用户偏好用localStorage服务端数据仍走工具。妥善处理加载态—— 展示 spinner、禁用按钮、按条目锁定loadingId。实现键盘快捷键—— 面向专业用户提升操作效率。用 React DevTools Profiler 做性能剖析—— 定位重复渲染与长任务。永远先查isPending再访问props—— 这是本技能反复强调的最高频错误。Widget 只管 UI 状态不要为选择/筛选创建工具—— 服务端状态才归工具管。优先用useCallTool而不是裸fetch—— 复用鉴权、校验与错误处理能力。14. 性能检查清单在交付复杂 Widget 前逐项核对大列表已虚拟化或分页昂贵计算已用useMemo记忆化事件处理器已用useCallback记忆化搜索输入已防抖图片已懒加载已放置错误边界控制台警告已处理此清单与文档中的 Best Practices 一一对应可作为代码评审的检查项直接使用。15. 下一步从模式到完整示例想系统学习交互与工具调用useCallTool回调、乐观更新、sendFollowUpMessage阅读 interactivity.md想了解状态归属与setState用法阅读 state.md想看到服务端 Widget端到端组合示例天气、待办、菜谱浏览阅读 common-patterns.md回顾全部开发约定与黄金法则回到技能总览 SKILL.md。在仓库中可直接对照的真实实现是 mcp-use-serversearch-toolsget-fruit-details的渲染工具 数据工具配对见 tools/product-search.ts正是本文所讲模式的最小完整闭环。以npm run dev启动后即可在 Inspector 中迭代验证错误边界、防抖与无限滚动等模式的运行效果。【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考