Advanced techniques for building complex, performant widgets.
Sign in to save downloads to your library and vote.
Preview
Advanced Widget Patterns
Advanced techniques for building complex, performant widgets.
Topics: Error boundaries, memoization, async data fetching, code splitting, complex state management
Error Boundaries
Catch React errors and display fallback UI:
import { 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 Component<ErrorBoundaryProps, 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" }}>
<h3>Something 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>
<div>Loading...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<ErrorBoundary>
<WidgetContent props={props} />
</ErrorBoundary>
</McpUseProvider>
);
}
useMemo for Performance
Memoize expensive computations:
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>
<div>Loading...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<p>Total: {sortedAndFiltered.total}</p>
<p>Average: {sortedAndFiltered.avgScore.toFixed(2)}</p>
{sortedAndFiltered.items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
</McpUseProvider>
);
}
useCallback for Stable Functions
Prevent unnecessary re-renders:
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] = useState<string | 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>
<div>Loading...</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 won't 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>
));
Async Data Fetching (Client-Side)
Fetch additional data from widget:
import { useState, useEffect } from "react";
import { McpUseProvider, useWidget } from "mcp-use/react";
export default function AsyncWidget() {
const { props, isPending } = useWidget();
const [details, setDetails] = useState<Record<string, 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>
<div>Loading...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<h2>{props.title}</h2>
{loading && <p>Loading details...</p>}
{details && (
<div>
<h3>Details</h3>
<pre>{JSON.stringify(details, null, 2)}</pre>
</div>
)}
</div>
</McpUseProvider>
);
}
Prefer tool calls over direct API calls:
// ✅ 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]);
Complex State Management
Use useReducer for complex state:
import { useReducer } from "react";
import { McpUseProvider, useWidget } from "mcp-use/react";
type State = {
selectedIds: Set<string>;
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>
<div>Loading...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<input
type="text"
value={state.filters.search}
onChange={(e) =>
dispatch({
type: "SET_FILTER",
key: "search",
value: e.target.value,
})
}
placeholder="Search..."
/>
<button onClick={() => dispatch({ type: "RESET" })}>
Reset Filters
</button>
{/* ... render items with state */}
</div>
</McpUseProvider>
);
}
Virtualization for Large Lists
Render only visible items:
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 = useRef<HTMLDivElement>(null);
const itemHeight = 50;
const containerHeight = 400;
const overscan = 3;
if (isPending) {
return (
<McpUseProvider autoSize>
<div>Loading...</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>
);
}
Debounced Search
Prerequisites: For interactive widgets (buttons, forms, tool calls), read interactivity.md first for foundational patterns.
Delay search to avoid excessive calls:
import { useState, useEffect } from "react";
import { McpUseProvider, useWidget, useCallTool } from "mcp-use/react";
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;
}
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>
<div>Loading...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
style={{ width: "100%", padding: 8 }}
/>
{searching && <p>Searching...</p>}
<div>
{results.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
</div>
</McpUseProvider>
);
}
Infinite Scroll
Load more items as user scrolls:
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] = useState<Item[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const observerTarget = useRef<HTMLDivElement>(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>
<div>Loading...</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 && <p>Loading more...</p>}
{!hasMore && <p>No more items</p>}
</div>
</div>
</McpUseProvider>
);
}
Local Storage Persistence
Persist widget state across sessions:
import { useState, useEffect } from "react";
import { McpUseProvider, useWidget } from "mcp-use/react";
function useLocalStorage<T>(
key: string,
initialValue: T,
): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
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] = useLocalStorage<string[]>("favorites", []);
const toggleFavorite = (id: string) => {
setFavorites((prev) =>
prev.includes(id) ? prev.filter((fav) => fav !== id) : [...prev, id],
);
};
if (isPending) {
return (
<McpUseProvider autoSize>
<div>Loading...</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>
);
}
Drag and Drop
Reorder items with drag and drop:
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] = useState<Item[]>([]);
const [draggedIndex, setDraggedIndex] = useState<number | 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>
<div>Loading...</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>
);
}
Keyboard Shortcuts
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) => {
// Ctrl+S 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>
<div>Loading...</div>
</McpUseProvider>
);
}
return (
<McpUseProvider autoSize>
<div>
<p>Keyboard shortcuts:</p>
<ul>
<li>
<kbd>Ctrl+S</kbd> - Save
</li>
<li>
<kbd>Esc</kbd> - Cancel
</li>
<li>
<kbd>↑/↓</kbd> - Navigate
</li>
</ul>
</div>
</McpUseProvider>
);
}
Best Practices
- Use Error Boundaries - Catch errors gracefully
- Memoize Expensive Computations - Use
useMemofor performance - Debounce User Input - Avoid excessive API calls
- Virtualize Large Lists - Render only visible items
- Persist State When Useful - Use localStorage for preferences
- Handle Loading States - Show spinners, disable buttons
- Implement Keyboard Shortcuts - Improve power user experience
- Profile Performance - Use React DevTools Profiler
Performance Checklist
- Large lists virtualized or paginated
- Expensive computations memoized with
useMemo - Event handlers memoized with
useCallback - Search inputs debounced
- Images lazy-loaded
- Error boundaries in place
- Console warnings addressed
Next Steps
- See examples → ../patterns/common-patterns.md
- Review best practices → ../../SKILL.md
Ingestion metadata
- Source catalog
- awesome-llm-apps
- Repository
- Shubhamsaboo/awesome-llm-apps · main
- File path
- generative_ui_agents/ai-mcp-app-builder/apps/mcp-use-server/.agent/skills/mcp-apps-builder/references/widgets/advanced.md
- Last refreshed
- 10/24/2026, 3:00:14 AM (5h ago)
- Refresh schedule
- Daily · 03:00 UTC
- Dedupe status
- Unique · deduped by (source, url)