Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
DEPRECATED: Replaced by mcp-app-builder. Previously used to build ChatGPT apps with interactive React widgets via mcp-use.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/widgets/advanced.md
1# Advanced Widget Patterns23Advanced techniques for building complex, performant widgets.45**Topics:** Error boundaries, memoization, async data fetching, code splitting, complex state management67---89## Error Boundaries1011Catch React errors and display fallback UI:1213```tsx14import { Component, ReactNode } from "react";15import { McpUseProvider, useWidget } from "mcp-use/react";1617interface ErrorBoundaryProps {18children: ReactNode;19}2021interface ErrorBoundaryState {22hasError: boolean;23error: Error | null;24}2526class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {27constructor(props: ErrorBoundaryProps) {28super(props);29this.state = { hasError: false, error: null };30}3132static getDerivedStateFromError(error: Error) {33return { hasError: true, error };34}3536componentDidCatch(error: Error, errorInfo: any) {37console.error("Widget error:", error, errorInfo);38}3940render() {41if (this.state.hasError) {42return (43<div style={{ padding: 20, color: "#c62828" }}>44<h3>Something went wrong</h3>45<p>{this.state.error?.message}</p>46<button onClick={() => this.setState({ hasError: false, error: null })}>47Try Again48</button>49</div>50);51}5253return this.props.children;54}55}5657// Usage58export default function SafeWidget() {59const { props, isPending } = useWidget();6061if (isPending) {62return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;63}6465return (66<McpUseProvider autoSize>67<ErrorBoundary>68<WidgetContent props={props} />69</ErrorBoundary>70</McpUseProvider>71);72}73```7475---7677## useMemo for Performance7879Memoize expensive computations:8081```tsx82import { useMemo } from "react";83import { McpUseProvider, useWidget } from "mcp-use/react";8485export default function OptimizedWidget() {86const { props, isPending } = useWidget();8788// Expensive computation - only runs when props.items changes89// Guard against isPending where props.items is undefined90const sortedAndFiltered = useMemo(() => {91if (!props.items) return { items: [], total: 0, avgScore: 0 };9293let result = props.items;9495// Filter96result = result.filter(item => item.active);9798// Sort99result.sort((a, b) => b.score - a.score);100101// Compute stats102return {103items: result,104total: result.length,105avgScore: result.length > 0106? result.reduce((sum, item) => sum + item.score, 0) / result.length107: 0108};109}, [props.items]);110111if (isPending) {112return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;113}114115return (116<McpUseProvider autoSize>117<div style={{ padding: 20 }}>118<p>Total: {sortedAndFiltered.total}</p>119<p>Average: {sortedAndFiltered.avgScore.toFixed(2)}</p>120121{sortedAndFiltered.items.map(item => (122<div key={item.id}>{item.name}</div>123))}124</div>125</McpUseProvider>126);127}128```129130---131132## useCallback for Stable Functions133134Prevent unnecessary re-renders:135136```tsx137import { useCallback, useState } from "react";138import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";139140export default function CallbackWidget() {141const { props, isPending } = useWidget();142const { callToolAsync } = useCallTool("process-item");143const [loadingId, setLoadingId] = useState<string | null>(null);144145// Stable function reference146const handleAction = useCallback(async (id: string) => {147setLoadingId(id);148try {149await callToolAsync({ id });150} finally {151setLoadingId(null);152}153}, [callToolAsync]);154155if (isPending) {156return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;157}158159return (160<McpUseProvider autoSize>161<div>162{props.items.map(item => (163<ItemRow164key={item.id}165item={item}166onAction={handleAction}167loading={loadingId === item.id}168/>169))}170</div>171</McpUseProvider>172);173}174175// Child component won't re-render unnecessarily176const ItemRow = React.memo(({ item, onAction, loading }: any) => (177<div>178<span>{item.name}</span>179<button onClick={() => onAction(item.id)} disabled={loading}>180{loading ? "Processing..." : "Process"}181</button>182</div>183));184```185186---187188## Async Data Fetching (Client-Side)189190Fetch additional data from widget:191192```tsx193import { useState, useEffect } from "react";194import { McpUseProvider, useWidget } from "mcp-use/react";195196export default function AsyncWidget() {197const { props, isPending } = useWidget();198const [details, setDetails] = useState<Record<string, unknown> | null>(null);199const [loading, setLoading] = useState(false);200201useEffect(() => {202if (!isPending && props.itemId) {203setLoading(true);204fetch(`/api/items/${props.itemId}/details`)205.then(res => res.json())206.then(data => setDetails(data))207.catch(err => console.error("Failed to load details:", err))208.finally(() => setLoading(false));209}210}, [isPending, props.itemId]);211212if (isPending) {213return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;214}215216return (217<McpUseProvider autoSize>218<div style={{ padding: 20 }}>219<h2>{props.title}</h2>220221{loading && <p>Loading details...</p>}222223{details && (224<div>225<h3>Details</h3>226<pre>{JSON.stringify(details, null, 2)}</pre>227</div>228)}229</div>230</McpUseProvider>231);232}233```234235**Prefer tool calls over direct API calls:**236```tsx237// ✅ Better - Use useCallTool238const { callToolAsync } = useCallTool("get-item-details");239240useEffect(() => {241if (!isPending && props.itemId) {242setLoading(true);243callToolAsync({ id: props.itemId })244.then(result => setDetails(result))245.finally(() => setLoading(false));246}247}, [isPending, props.itemId, callToolAsync]);248```249250---251252## Complex State Management253254Use useReducer for complex state:255256```tsx257import { useReducer } from "react";258import { McpUseProvider, useWidget } from "mcp-use/react";259260type State = {261selectedIds: Set<string>;262filters: { category: string; search: string };263sortBy: string;264sortOrder: "asc" | "desc";265};266267type Action =268| { type: "TOGGLE_SELECT"; id: string }269| { type: "SET_FILTER"; key: string; value: string }270| { type: "SET_SORT"; by: string }271| { type: "TOGGLE_SORT_ORDER" }272| { type: "RESET" };273274function reducer(state: State, action: Action): State {275switch (action.type) {276case "TOGGLE_SELECT":277const newSelection = new Set(state.selectedIds);278if (newSelection.has(action.id)) {279newSelection.delete(action.id);280} else {281newSelection.add(action.id);282}283return { ...state, selectedIds: newSelection };284285case "SET_FILTER":286return {287...state,288filters: { ...state.filters, [action.key]: action.value }289};290291case "SET_SORT":292return { ...state, sortBy: action.by };293294case "TOGGLE_SORT_ORDER":295return {296...state,297sortOrder: state.sortOrder === "asc" ? "desc" : "asc"298};299300case "RESET":301return {302selectedIds: new Set(),303filters: { category: "all", search: "" },304sortBy: "name",305sortOrder: "asc"306};307308default:309return state;310}311}312313export default function ComplexWidget() {314const { props, isPending } = useWidget();315const [state, dispatch] = useReducer(reducer, {316selectedIds: new Set(),317filters: { category: "all", search: "" },318sortBy: "name",319sortOrder: "asc"320});321322if (isPending) {323return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;324}325326return (327<McpUseProvider autoSize>328<div style={{ padding: 20 }}>329<input330type="text"331value={state.filters.search}332onChange={e => dispatch({ type: "SET_FILTER", key: "search", value: e.target.value })}333placeholder="Search..."334/>335336<button onClick={() => dispatch({ type: "RESET" })}>337Reset Filters338</button>339340{/* ... render items with state */}341</div>342</McpUseProvider>343);344}345```346347---348349## Virtualization for Large Lists350351Render only visible items:352353```tsx354import { useState, useRef, useEffect } from "react";355import { McpUseProvider, useWidget } from "mcp-use/react";356357export default function VirtualizedList() {358const { props, isPending } = useWidget();359const [scrollTop, setScrollTop] = useState(0);360const containerRef = useRef<HTMLDivElement>(null);361362const itemHeight = 50;363const containerHeight = 400;364const overscan = 3;365366if (isPending) {367return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;368}369370const visibleStart = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);371const visibleEnd = Math.min(372props.items.length,373Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan374);375376const visibleItems = props.items.slice(visibleStart, visibleEnd);377378return (379<McpUseProvider autoSize>380<div381ref={containerRef}382onScroll={e => setScrollTop(e.currentTarget.scrollTop)}383style={{384height: containerHeight,385overflow: "auto",386position: "relative"387}}388>389<div style={{ height: props.items.length * itemHeight, position: "relative" }}>390{visibleItems.map((item, index) => (391<div392key={item.id}393style={{394position: "absolute",395top: (visibleStart + index) * itemHeight,396height: itemHeight,397width: "100%",398padding: 12,399borderBottom: "1px solid #eee"400}}401>402{item.name}403</div>404))}405</div>406</div>407</McpUseProvider>408);409}410```411412---413414## Debounced Search415416> **Prerequisites:** For interactive widgets (buttons, forms, tool calls), read [interactivity.md](interactivity.md) first for foundational patterns.417418Delay search to avoid excessive calls:419420```tsx421import { useState, useEffect } from "react";422import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";423424function useDebounce<T>(value: T, delay: number): T {425const [debouncedValue, setDebouncedValue] = useState<T>(value);426427useEffect(() => {428const handler = setTimeout(() => {429setDebouncedValue(value);430}, delay);431432return () => {433clearTimeout(handler);434};435}, [value, delay]);436437return debouncedValue;438}439440export default function DebouncedSearchWidget() {441const { props, isPending } = useWidget();442const { callToolAsync } = useCallTool("search");443const [search, setSearch] = useState("");444const [results, setResults] = useState<{ id: string; name: string }[]>([]);445const [searching, setSearching] = useState(false);446447const debouncedSearch = useDebounce(search, 300);448449useEffect(() => {450if (!debouncedSearch.trim()) {451setResults([]);452return;453}454455setSearching(true);456callToolAsync({ query: debouncedSearch })457.then(result => setResults(result.structuredContent?.items || []))458.catch(err => console.error("Search failed:", err))459.finally(() => setSearching(false));460}, [debouncedSearch, callToolAsync]);461462if (isPending) {463return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;464}465466return (467<McpUseProvider autoSize>468<div style={{ padding: 20 }}>469<input470type="text"471value={search}472onChange={e => setSearch(e.target.value)}473placeholder="Search..."474style={{ width: "100%", padding: 8 }}475/>476477{searching && <p>Searching...</p>}478479<div>480{results.map(item => (481<div key={item.id}>{item.name}</div>482))}483</div>484</div>485</McpUseProvider>486);487}488```489490---491492## Infinite Scroll493494Load more items as user scrolls:495496```tsx497import { useState, useRef, useEffect } from "react";498import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";499500interface Item {501id: string;502name: string;503}504505export default function InfiniteScrollWidget() {506const { props, isPending } = useWidget<{ items: Item[] }>();507const { callToolAsync } = useCallTool("load-more");508const [items, setItems] = useState<Item[]>([]);509const [loading, setLoading] = useState(false);510const [hasMore, setHasMore] = useState(true);511const observerTarget = useRef<HTMLDivElement>(null);512513// Sync initial items from props once loaded514useEffect(() => {515if (!isPending && props.items) {516setItems(props.items);517}518}, [isPending, props.items]);519520useEffect(() => {521const observer = new IntersectionObserver(522entries => {523if (entries[0].isIntersecting && hasMore && !loading) {524loadMore();525}526},527{ threshold: 1.0 }528);529530if (observerTarget.current) {531observer.observe(observerTarget.current);532}533534return () => observer.disconnect();535}, [hasMore, loading]);536537const loadMore = async () => {538setLoading(true);539try {540const result = await callToolAsync({541offset: items.length,542limit: 20543});544545const newItems = result.structuredContent?.items || [];546if (newItems.length === 0) {547setHasMore(false);548} else {549setItems(prev => [...prev, ...newItems]);550}551} catch (error) {552console.error("Failed to load more:", error);553} finally {554setLoading(false);555}556};557558if (isPending) {559return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;560}561562return (563<McpUseProvider autoSize>564<div style={{ padding: 20 }}>565{items.map(item => (566<div key={item.id} style={{ padding: 12, borderBottom: "1px solid #eee" }}>567{item.name}568</div>569))}570571<div ref={observerTarget} style={{ height: 20 }}>572{loading && <p>Loading more...</p>}573{!hasMore && <p>No more items</p>}574</div>575</div>576</McpUseProvider>577);578}579```580581---582583## Local Storage Persistence584585Persist widget state across sessions:586587```tsx588import { useState, useEffect } from "react";589import { McpUseProvider, useWidget } from "mcp-use/react";590591function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {592const [storedValue, setStoredValue] = useState<T>(() => {593try {594const item = window.localStorage.getItem(key);595return item ? JSON.parse(item) : initialValue;596} catch (error) {597console.error("Error reading from localStorage:", error);598return initialValue;599}600});601602const setValue = (value: T) => {603try {604setStoredValue(value);605window.localStorage.setItem(key, JSON.stringify(value));606} catch (error) {607console.error("Error writing to localStorage:", error);608}609};610611return [storedValue, setValue];612}613614export default function PersistentWidget() {615const { props, isPending } = useWidget();616const [favorites, setFavorites] = useLocalStorage<string[]>("favorites", []);617618const toggleFavorite = (id: string) => {619setFavorites(prev =>620prev.includes(id) ? prev.filter(fav => fav !== id) : [...prev, id]621);622};623624if (isPending) {625return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;626}627628return (629<McpUseProvider autoSize>630<div>631{props.items.map(item => (632<div key={item.id}>633<button onClick={() => toggleFavorite(item.id)}>634{favorites.includes(item.id) ? "⭐" : "☆"}635</button>636{item.name}637</div>638))}639</div>640</McpUseProvider>641);642}643```644645---646647## Drag and Drop648649Reorder items with drag and drop:650651```tsx652import { useState, useEffect } from "react";653import { McpUseProvider, useWidget } from "mcp-use/react";654655interface Item {656id: string;657name: string;658}659660export default function DraggableList() {661const { props, isPending } = useWidget<{ items: Item[] }>();662const [items, setItems] = useState<Item[]>([]);663const [draggedIndex, setDraggedIndex] = useState<number | null>(null);664665// Sync items from props once loaded666useEffect(() => {667if (!isPending && props.items) {668setItems(props.items);669}670}, [isPending, props.items]);671672const handleDragStart = (index: number) => {673setDraggedIndex(index);674};675676const handleDragOver = (e: React.DragEvent, index: number) => {677e.preventDefault();678679if (draggedIndex === null || draggedIndex === index) return;680681const newItems = [...items];682const draggedItem = newItems[draggedIndex];683684newItems.splice(draggedIndex, 1);685newItems.splice(index, 0, draggedItem);686687setItems(newItems);688setDraggedIndex(index);689};690691const handleDragEnd = () => {692setDraggedIndex(null);693// Optionally save new order with useCallTool694};695696if (isPending) {697return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;698}699700return (701<McpUseProvider autoSize>702<div>703{items.map((item, index) => (704<div705key={item.id}706draggable707onDragStart={() => handleDragStart(index)}708onDragOver={e => handleDragOver(e, index)}709onDragEnd={handleDragEnd}710style={{711padding: 12,712margin: "4px 0",713backgroundColor: draggedIndex === index ? "#e3f2fd" : "white",714border: "1px solid #ddd",715cursor: "move"716}}717>718⋮⋮ {item.name}719</div>720))}721</div>722</McpUseProvider>723);724}725```726727---728729## Keyboard Shortcuts730731```tsx732import { useEffect } from "react";733import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";734735export default function KeyboardWidget() {736const { props, isPending } = useWidget();737const { callTool: save } = useCallTool("save");738739useEffect(() => {740const handleKeyDown = (e: KeyboardEvent) => {741// Ctrl+S to save742if (e.ctrlKey && e.key === "s") {743e.preventDefault();744save({});745}746747// Escape to cancel748if (e.key === "Escape") {749// Handle escape750}751752// Arrow keys for navigation753if (e.key === "ArrowDown") {754// Navigate down755}756};757758window.addEventListener("keydown", handleKeyDown);759return () => window.removeEventListener("keydown", handleKeyDown);760}, [save]);761762if (isPending) {763return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;764}765766return (767<McpUseProvider autoSize>768<div>769<p>Keyboard shortcuts:</p>770<ul>771<li><kbd>Ctrl+S</kbd> - Save</li>772<li><kbd>Esc</kbd> - Cancel</li>773<li><kbd>↑/↓</kbd> - Navigate</li>774</ul>775</div>776</McpUseProvider>777);778}779```780781---782783## Best Practices7847851. **Use Error Boundaries** - Catch errors gracefully7862. **Memoize Expensive Computations** - Use `useMemo` for performance7873. **Debounce User Input** - Avoid excessive API calls7884. **Virtualize Large Lists** - Render only visible items7895. **Persist State When Useful** - Use localStorage for preferences7906. **Handle Loading States** - Show spinners, disable buttons7917. **Implement Keyboard Shortcuts** - Improve power user experience7928. **Profile Performance** - Use React DevTools Profiler793794---795796## Performance Checklist797798- [ ] Large lists virtualized or paginated799- [ ] Expensive computations memoized with `useMemo`800- [ ] Event handlers memoized with `useCallback`801- [ ] Search inputs debounced802- [ ] Images lazy-loaded803- [ ] Error boundaries in place804- [ ] Console warnings addressed805806---807808## Next Steps809810- **See examples** → [../patterns/common-patterns.md](../patterns/common-patterns.md)811- **Review best practices** → [../../SKILL.md](../../SKILL.md)812