Migrate to useWidgetSDK
Migrate widget SDK versions
This guide helps you migrate from the legacy withToolData HOC pattern to the modern useWidgetSDK hook-based approach. The new SDK provides better type safety, more flexibility, and cleaner code.
Why Migrate?
withToolData still works, but the hooks are simpler and give widgets more to work with.
What changes
useWidgetSDK is a hook, so your component no longer needs the withToolData wrapper. It gives you typed access to the tool output through generics, direct access to every SDK method, and the host's context, such as the theme and display mode.
Backward Compatibility
Note
withToolData is still supported and will continue to work. This migration is optional but recommended for new widgets.
Quick Migration
The same widget before and after.
Before (withToolData)
The wrapper passes the tool's output in as props:
'use client';
import { withToolData } from '@nitrostack/widgets';
interface ProductData {
id: string;
name: string;
price: number;
}
function ProductWidget({ data }: { data: ProductData }) {
return (
<div>
<h2>{data.name}</h2>
<p>${data.price}</p>
</div>
);
}
export default withToolData(ProductWidget);After (useWidgetSDK)
The hook reads the output inside the component:
'use client';
import { useWidgetSDK } from '@nitrostack/widgets';
interface ProductData {
id: string;
name: string;
price: number;
}
export default function ProductWidget() {
const { isReady, getToolOutput } = useWidgetSDK();
if (!isReady) {
return <div>Loading...</div>;
}
const data = getToolOutput<ProductData>();
return (
<div>
<h2>{data.name}</h2>
<p>${data.price}</p>
</div>
);
}Step-by-Step Migration
Migrate one widget at a time with these steps.
Remove withToolData
Before:
import { withToolData } from '@nitrostack/widgets';
function MyWidget({ data }) {
return <div>{data.title}</div>;
}
export default withToolData(MyWidget);After:
import { useWidgetSDK } from '@nitrostack/widgets';
export default function MyWidget() {
const { getToolOutput } = useWidgetSDK();
const data = getToolOutput();
return <div>{data.title}</div>;
}Add Loading State
The HOC handled loading automatically. With hooks, you control it:
export default function MyWidget() {
const { isReady, getToolOutput } = useWidgetSDK();
// Add loading state
if (!isReady) {
return <div>Loading...</div>;
}
const data = getToolOutput();
return <div>{data.title}</div>;
}Add Type Safety
Use TypeScript generics for better type checking:
interface MyData {
title: string;
count: number;
}
export default function MyWidget() {
const { isReady, getToolOutput } = useWidgetSDK();
if (!isReady) {
return <div>Loading...</div>;
}
// Type-safe data access
const data = getToolOutput<MyData>();
return (
<div>
<h1>{data.title}</h1>
<p>Count: {data.count}</p>
</div>
);
}Common Patterns
How typical widgets change.
Pattern 1: Simple Data Display
Before:
import { withToolData } from '@nitrostack/widgets';
function UserProfile({ data }) {
return (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
</div>
);
}
export default withToolData(UserProfile);After:
import { useWidgetSDK } from '@nitrostack/widgets';
interface UserData {
name: string;
email: string;
}
export default function UserProfile() {
const { isReady, getToolOutput } = useWidgetSDK();
if (!isReady) return <div>Loading...</div>;
const data = getToolOutput<UserData>();
return (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
</div>
);
}Pattern 2: Interactive Widget
Before:
import { withToolData } from '@nitrostack/widgets';
function ProductCard({ data }) {
const handleClick = () => {
// Limited access to SDK
console.log('Clicked', data.id);
};
return (
<div onClick={handleClick}>
<h2>{data.name}</h2>
</div>
);
}
export default withToolData(ProductCard);After:
import { useWidgetSDK } from '@nitrostack/widgets';
interface ProductData {
id: string;
name: string;
}
export default function ProductCard() {
const { isReady, getToolOutput, callTool, sendFollowUpMessage } = useWidgetSDK();
if (!isReady) return <div>Loading...</div>;
const data = getToolOutput<ProductData>();
const handleClick = async () => {
// Full SDK access
await callTool('view_product', { id: data.id });
await sendFollowUpMessage(`Tell me more about ${data.name}`);
};
return (
<div onClick={handleClick}>
<h2>{data.name}</h2>
</div>
);
}Pattern 3: Theme-Aware Widget
Before:
import { withToolData } from '@nitrostack/widgets';
function ThemedWidget({ data }) {
// No easy access to theme
return (
<div style={{ background: '#000', color: '#fff' }}>
{data.content}
</div>
);
}
export default withToolData(ThemedWidget);After:
import { useWidgetSDK, useTheme } from '@nitrostack/widgets';
export default function ThemedWidget() {
const { isReady, getToolOutput } = useWidgetSDK();
const theme = useTheme();
if (!isReady) return <div>Loading...</div>;
const data = getToolOutput();
const styles = {
background: theme === 'dark' ? '#000' : '#fff',
color: theme === 'dark' ? '#fff' : '#000'
};
return (
<div style={styles}>
{data.content}
</div>
);
}Pattern 4: Responsive Widget
Before:
import { withToolData } from '@nitrostack/widgets';
function ResponsiveWidget({ data }) {
// No access to display mode
return <div>{data.content}</div>;
}
export default withToolData(ResponsiveWidget);After:
import { useWidgetSDK, useDisplayMode } from '@nitrostack/widgets';
export default function ResponsiveWidget() {
const { isReady, getToolOutput } = useWidgetSDK();
const displayMode = useDisplayMode();
if (!isReady) return <div>Loading...</div>;
const data = getToolOutput();
const padding = displayMode === 'fullscreen' ? '48px' : '16px';
const fontSize = displayMode === 'fullscreen' ? '24px' : '16px';
return (
<div style={{ padding, fontSize }}>
{data.content}
</div>
);
}Advanced Migration
Widgets that used several props move to several hooks.
Accessing Multiple SDK Features
Read the theme, display mode, and available height alongside the data:
import { useWidgetSDK, useTheme, useDisplayMode, useMaxHeight } from '@nitrostack/widgets';
export default function AdvancedWidget() {
const {
isReady,
getToolOutput,
callTool,
requestFullscreen,
setState,
getState
} = useWidgetSDK();
const theme = useTheme();
const displayMode = useDisplayMode();
const maxHeight = useMaxHeight();
if (!isReady) return <div>Loading...</div>;
const data = getToolOutput();
// Use all features together
const styles = {
background: theme === 'dark' ? '#000' : '#fff',
padding: displayMode === 'fullscreen' ? '48px' : '16px',
maxHeight: maxHeight ? `${maxHeight}px` : 'none',
overflow: 'auto'
};
return (
<div style={styles}>
<button onClick={requestFullscreen}>Fullscreen</button>
{data.content}
</div>
);
}State Management
Before:
import { withToolData } from '@nitrostack/widgets';
import { useState } from 'react';
function StatefulWidget({ data }) {
const [count, setCount] = useState(0);
// State not persisted
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default withToolData(StatefulWidget);After:
import { useWidgetSDK, useWidgetState } from '@nitrostack/widgets';
interface WidgetState {
count: number;
}
export default function StatefulWidget() {
const { isReady, getToolOutput } = useWidgetSDK();
const { state, setState } = useWidgetState<WidgetState>();
if (!isReady) return <div>Loading...</div>;
const data = getToolOutput();
const count = state?.count || 0;
const increment = async () => {
await setState({ count: count + 1 });
// State persisted automatically
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}Troubleshooting
Problems you may see right after migrating.
Widget Shows "Loading..." Forever
Problem: isReady never becomes true
Solution: Ensure widget is being rendered in proper context (Studio or ChatGPT)
// Add debugging
const { isReady } = useWidgetSDK();
useEffect(() => {
console.log('SDK ready:', isReady);
}, [isReady]);Data is Null
Problem: getToolOutput() returns null
Solution: Check that tool is returning data correctly
const data = getToolOutput();
if (!data) {
return <div>No data available</div>;
}TypeScript Errors
Problem: Type errors with getToolOutput()
Solution: Provide explicit type parameter
// Before (error)
const data = getToolOutput();
data.name; // TypeScript error
// After (fixed)
interface MyData {
name: string;
}
const data = getToolOutput<MyData>();
data.name; // TypeScript knows the typePerformance Considerations
useWidgetSDK() returns a single shared SDK instance, so calling it in several components doesn't create new ones. useTheme() and useDisplayMode() re-render your component only when the theme or the display mode changes, and useWidgetState() batches state updates.
A component wrapped in withToolData re-renders whenever any of its props change. With the hooks you decide what triggers a render, and you can add React.memo where a complex widget needs it.
