Setup
Ensure your tsconfig.json supports JSX:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
2. Install React
React is a peer dependency:
npm install react react-dom
npm install -D @types/react @types/react-dom
Basic Usage
Create a component file:
src/widgets/weather-widget.tsx
import { TemplateContext } from '@frontmcp/ui';
interface WeatherOutput {
location: string;
temperature: number;
conditions: string;
}
export function WeatherWidget({ output }: TemplateContext<unknown, WeatherOutput>) {
return (
<div className="p-6 bg-white rounded-xl shadow-lg">
<h2 className="text-2xl font-bold">{output.location}</h2>
<div className="text-4xl my-4">{output.temperature}°C</div>
<p className="text-gray-600">{output.conditions}</p>
</div>
);
}
Use it in your tool:
src/tools/weather.tool.ts
import { Tool, ToolContext } from '@frontmcp/sdk';
import { WeatherWidget } from '../widgets/weather-widget';
@Tool({
name: 'get_weather',
ui: {
template: WeatherWidget,
},
})
export class GetWeatherTool extends ToolContext {
// ...
}
Props Interface
React templates receive the same context as other templates:
import { TemplateContext } from '@frontmcp/ui';
// Define your input/output types
interface MyInput {
userId: string;
}
interface MyOutput {
name: string;
email: string;
}
// Use them in the component
export function MyWidget({ input, output, helpers }: TemplateContext<MyInput, MyOutput>) {
return (
<div>
<p>User ID: {input.userId}</p>
<p>Name: {output.name}</p>
<p>Email: {output.email}</p>
</div>
);
}
Using Helpers
The helpers prop provides utility functions:
export function OrderWidget({ output, helpers }: TemplateContext<unknown, Order>) {
return (
<div>
<p>Order Date: {helpers.formatDate(output.date)}</p>
<p>Total: {helpers.formatCurrency(output.total, 'USD')}</p>
</div>
);
}
React automatically escapes content, so you don’t need helpers.escapeHtml() for JSX text content.
Only use it when dangerously setting innerHTML.
Using FrontMCP Components
Import and use string components:
import { card, badge, button } from '@frontmcp/ui';
export function ProfileWidget({ output }: TemplateContext<unknown, User>) {
// Components return HTML strings - use dangerouslySetInnerHTML
const statusBadge = badge(output.status, {
variant: output.status === 'Active' ? 'success' : 'default',
});
return (
<div className="p-4">
<div className="flex items-center gap-3">
<h2 className="text-xl font-bold">{output.name}</h2>
<span dangerouslySetInnerHTML={{ __html: statusBadge }} />
</div>
{/* Or wrap the whole thing */}
<div
dangerouslySetInnerHTML={{
__html: card(output.bio, { title: 'About' }),
}}
/>
</div>
);
}
Creating React Wrappers
For cleaner code, wrap string components:
src/widgets/components.tsx
import { button as htmlButton, ButtonOptions } from '@frontmcp/ui';
// React wrapper for button
export function Button({ children, ...props }: ButtonOptions & { children: string }) {
return (
<span dangerouslySetInnerHTML={{ __html: htmlButton(children, props) }} />
);
}
// Usage
<Button variant="primary" onClick="submit()">Submit</Button>
Conditional Rendering
Use standard React patterns:
export function StatusWidget({ output }: TemplateContext<unknown, Status>) {
if (output.loading) {
return (
<div className="flex justify-center p-8">
<div className="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" />
</div>
);
}
if (output.error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800">{output.error}</p>
</div>
);
}
return (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<p className="text-green-800">Success!</p>
</div>
);
}
Lists and Mapping
interface ListOutput {
items: Array<{ id: string; name: string; price: number }>;
}
export function ItemList({ output, helpers }: TemplateContext<unknown, ListOutput>) {
if (output.items.length === 0) {
return <p className="text-gray-500 text-center py-4">No items found</p>;
}
return (
<ul className="divide-y">
{output.items.map((item) => (
<li key={item.id} className="py-3 flex justify-between">
<span>{item.name}</span>
<span>{helpers.formatCurrency(item.price)}</span>
</li>
))}
</ul>
);
}
Component Composition
Split complex widgets into smaller components:
src/widgets/order/index.tsx
import { TemplateContext } from '@frontmcp/ui';
import { OrderHeader } from './order-header';
import { OrderItems } from './order-items';
import { OrderTotal } from './order-total';
export function OrderWidget({ output, helpers }: TemplateContext<unknown, Order>) {
return (
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
<OrderHeader order={output} />
<OrderItems items={output.items} formatCurrency={helpers.formatCurrency} />
<OrderTotal total={output.total} formatCurrency={helpers.formatCurrency} />
</div>
);
}
Styling
Tailwind Classes
export function Card({ output }: TemplateContext<unknown, CardData>) {
return (
<div className="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
<h3 className="text-lg font-semibold text-gray-800">{output.title}</h3>
<p className="mt-2 text-gray-600">{output.description}</p>
</div>
);
}
CSS Modules
import styles from './widget.module.css';
export function Widget({ output }: TemplateContext<unknown, Data>) {
return (
<div className={styles.container}>
<h2 className={styles.title}>{output.title}</h2>
</div>
);
}
Inline Styles
export function Badge({ text, color }: { text: string; color: string }) {
return (
<span
style={{
backgroundColor: `${color}20`,
color: color,
padding: '2px 8px',
borderRadius: '9999px',
fontSize: '12px',
}}
>
{text}
</span>
);
}
TypeScript Integration
Full type safety with generics:
import { TemplateContext } from '@frontmcp/ui';
import { z } from 'zod';
// Define schemas using z.object()
const inputSchema = z.object({
userId: z.string(),
});
const outputSchema = z.object({
user: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
role: z.enum(['admin', 'user', 'guest']),
}),
});
// Infer types from schemas
type Input = z.infer<typeof inputSchema>;
type Output = z.infer<typeof outputSchema>;
// Fully typed component
export function UserWidget({ input, output }: TemplateContext<Input, Output>) {
const { user } = output;
return (
<div>
<p>Requested: {input.userId}</p>
<p>Name: {user.name}</p>
<p>Role: {user.role}</p> {/* TypeScript knows this is 'admin' | 'user' | 'guest' */}
</div>
);
}
Server-Side Rendering
React templates are rendered server-side by default using react-dom/server:
// FrontMCP automatically does this:
import { renderToString } from 'react-dom/server';
const html = renderToString(<YourWidget {...context} />);
For client-side interactivity, see Hydration.
Best Practices
- Keep widgets pure - Avoid side effects, use props only
- Type everything - Use TypeScript for better DX
- Split large widgets - Compose from smaller components
- Don’t use hooks for SSR - They won’t work without hydration
- Escape dangerouslySetInnerHTML - When using string components
// ✅ Good - pure, typed, composed
export function Dashboard({ output }: TemplateContext<unknown, DashboardData>) {
return (
<div className="space-y-4">
<StatsCards stats={output.stats} />
<RecentActivity items={output.activity} />
<QuickActions />
</div>
);
}
// ❌ Avoid - hooks without hydration
export function BadWidget({ output }) {
const [count, setCount] = useState(0); // Won't work in SSR!
// ...
}