# Widget Patterns
Advanced widget patterns and examples for ChatGPT apps.
## Table of Contents
- [Complex Widget (Folder Structure)](#complex-widget-folder-structure)
- [Data Fetching Widget](#data-fetching-widget)
- [Stateful Widget](#stateful-widget)
- [Themed Widget](#themed-widget)
- [Widget with Tool Calls](#widget-with-tool-calls)
## Complex Widget (Folder Structure)
For widgets with multiple components:
```
resources/
└── product-search/
├── widget.tsx # Entry point (required name)
├── components/
│ ├── ProductCard.tsx
│ └── FilterBar.tsx
├── hooks/
│ └── useFilter.ts
├── types.ts
└── constants.ts
```
**Entry point (`widget.tsx`):**
```tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
import { ProductCard } from "./components/ProductCard";
import { FilterBar } from "./components/FilterBar";
export const widgetMetadata: WidgetMetadata = {
description: "Display product search results with filtering",
props: z.object({
products: z.array(
z.object({
id: z.string(),
name: z.string(),
price: z.number(),
image: z.string(),
}),
),
query: z.string(),
}),
};
const ProductSearch: React.FC = () => {
const { props, isPending, state, setState } = useWidget();
if (isPending) {
return (
Loading...
);
}
return (
Search: {props.query}
setState({ filters })} />
{props.products.map((p) => (
))}
);
};
export default ProductSearch;
```
## Data Fetching Widget
Widget that refreshes data via tool calls:
```tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Display data with refresh capability",
props: z.object({
id: z.string(),
title: z.string(),
data: z.array(z.any()),
}),
};
const DataWidget: React.FC = () => {
const { props, isPending, callTool } = useWidget();
if (isPending) return
Loading...
;
const refresh = async () => {
try {
await callTool("fetch-data", { id: props.id });
} catch (error) {
console.error("Refresh failed:", error);
}
};
return (
{props.title}
{props.data.map((item, i) => (
- {JSON.stringify(item)}
))}
);
};
export default DataWidget;
```
## Stateful Widget
Widget with persistent state across interactions:
```tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Counter with persistent state",
props: z.object({
initialValue: z.number().optional(),
}),
};
const CounterWidget: React.FC = () => {
const { props, isPending, state, setState } = useWidget();
if (isPending) return Loading...
;
const count = state?.count ?? props.initialValue ?? 0;
const increment = async () => {
await setState({ count: count + 1 });
};
const decrement = async () => {
await setState({ count: count - 1 });
};
return (
{count}
);
};
export default CounterWidget;
```
## Themed Widget
Widget that respects light/dark theme:
```tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Theme-aware content display",
props: z.object({
title: z.string(),
content: z.string(),
}),
};
const ThemedWidget: React.FC = () => {
const { props, isPending, theme } = useWidget();
if (isPending) return Loading...
;
const isDark = theme === "dark";
return (
{props.title}
{props.content}
);
};
export default ThemedWidget;
```
## Widget with Tool Calls
Widget that calls other MCP tools:
```tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
import { useState } from "react";
export const widgetMetadata: WidgetMetadata = {
description: "City weather lookup with search",
props: z.object({
defaultCity: z.string().optional(),
}),
};
const WeatherSearch: React.FC = () => {
const { props, isPending, callTool, state, setState } = useWidget();
const [loading, setLoading] = useState(false);
const [city, setCity] = useState(props.defaultCity || "");
if (isPending) return Loading...
;
const searchWeather = async () => {
if (!city.trim()) return;
setLoading(true);
try {
const result = await callTool("get-weather", { city });
await setState({ lastResult: result.content, lastCity: city });
} catch (error) {
console.error("Search failed:", error);
} finally {
setLoading(false);
}
};
return (
setCity(e.target.value)}
placeholder="Enter city name"
className="border rounded px-2 py-1"
/>
{state?.lastResult && (
Weather in {state.lastCity}:
{JSON.stringify(state.lastResult, null, 2)}
)}
);
};
export default WeatherSearch;
```