Runtime Support: In MCP Apps,
useWidgetState is polyfilled. State does not persist across widget renders.useWidgetState hook provides persistent state management for your widget. Unlike React’s useState, this state is persisted by the host and survives across widget re-renders and display mode changes.
Basic usage
Copy
Ask AI
import { useWidgetState } from "skybridge/web";
function CounterWidget() {
const [state, setState] = useWidgetState({ count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => setState((prev) => ({ count: prev.count + 1 }))}>
Increment
</button>
</div>
);
}
Parameters
defaultState
Copy
Ask AI
defaultState?: T | (() => T | null) | null
- A value of type
T - A function that returns
Tornull(lazy initialization) nullorundefined
Type Parameters
T
Copy
Ask AI
T extends Record<string, unknown>
Returns
Copy
Ask AI
[state: T | null, setState: (state: SetStateAction<T | null>) => void]
state
The current widget state, or null if no state is set.
setState
A function to update the state. Accepts either:
- A new state object
- A function that receives the previous state and returns the new state
Examples
Form State Persistence
Copy
Ask AI
import { useWidgetState } from "skybridge/web";
type FormState = {
name: string;
email: string;
step: number;
};
function MultiStepForm() {
const [form, setForm] = useWidgetState<FormState>({
name: "",
email: "",
step: 1,
});
const updateField = (field: keyof FormState, value: string | number) => {
setForm((prev) => ({ ...prev, [field]: value }));
};
if (form.step === 1) {
return (
<div>
<h3>Step 1: Name</h3>
<input
value={form.name}
onChange={(e) => updateField("name", e.target.value)}
placeholder="Your name"
/>
<button
onClick={() => updateField("step", 2)}
disabled={!form.name}
>
Next
</button>
</div>
);
}
if (form.step === 2) {
return (
<div>
<h3>Step 2: Email</h3>
<input
type="email"
value={form.email}
onChange={(e) => updateField("email", e.target.value)}
placeholder="Your email"
/>
<button onClick={() => updateField("step", 1)}>Back</button>
<button
onClick={() => updateField("step", 3)}
disabled={!form.email}
>
Submit
</button>
</div>
);
}
return (
<div>
<h3>Thank you, {form.name}!</h3>
<p>We'll contact you at {form.email}</p>
</div>
);
}
Shopping Cart
Copy
Ask AI
import { useWidgetState } from "skybridge/web";
type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
};
type CartState = {
items: CartItem[];
};
function ShoppingCart() {
const [cart, setCart] = useWidgetState<CartState>({ items: [] });
const addItem = (item: Omit<CartItem, "quantity">) => {
setCart((prev) => {
const existing = prev.items.find((i) => i.id === item.id);
if (existing) {
return {
items: prev.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...prev.items, { ...item, quantity: 1 }] };
});
};
const removeItem = (id: string) => {
setCart((prev) => ({
items: prev.items.filter((i) => i.id !== id),
}));
};
const total = cart.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<div className="cart">
<h3>Shopping Cart</h3>
{cart.items.length === 0 ? (
<p>Your cart is empty</p>
) : (
<>
<ul>
{cart.items.map((item) => (
<li key={item.id}>
{item.name} x{item.quantity} - ${item.price * item.quantity}
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
<p className="total">Total: ${total}</p>
</>
)}
</div>
);
}
User Preferences
Copy
Ask AI
import { useWidgetState } from "skybridge/web";
type Preferences = {
fontSize: "small" | "medium" | "large";
showImages: boolean;
sortBy: "date" | "name" | "relevance";
};
function PreferencesWidget() {
const [prefs, setPrefs] = useWidgetState<Preferences>({
fontSize: "medium",
showImages: true,
sortBy: "date",
});
return (
<div className="preferences">
<h3>Preferences</h3>
<label>
Font Size:
<select
value={prefs.fontSize}
onChange={(e) =>
setPrefs((prev) => ({
...prev,
fontSize: e.target.value as Preferences["fontSize"],
}))
}
>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</label>
<label>
<input
type="checkbox"
checked={prefs.showImages}
onChange={(e) =>
setPrefs((prev) => ({ ...prev, showImages: e.target.checked }))
}
/>
Show Images
</label>
<label>
Sort By:
<select
value={prefs.sortBy}
onChange={(e) =>
setPrefs((prev) => ({
...prev,
sortBy: e.target.value as Preferences["sortBy"],
}))
}
>
<option value="date">Date</option>
<option value="name">Name</option>
<option value="relevance">Relevance</option>
</select>
</label>
</div>
);
}
Lazy Initialization
Copy
Ask AI
import { useWidgetState } from "skybridge/web";
type GameState = {
board: string[][];
currentPlayer: "X" | "O";
winner: string | null;
};
function TicTacToe() {
const [game, setGame] = useWidgetState<GameState>(() => ({
board: [
["", "", ""],
["", "", ""],
["", "", ""],
],
currentPlayer: "X",
winner: null,
}));
const handleClick = (row: number, col: number) => {
if (game.board[row][col] || game.winner) return;
setGame((prev) => {
const newBoard = prev.board.map((r, i) =>
r.map((c, j) => (i === row && j === col ? prev.currentPlayer : c))
);
return {
board: newBoard,
currentPlayer: prev.currentPlayer === "X" ? "O" : "X",
winner: checkWinner(newBoard),
};
});
};
const resetGame = () => {
setGame({
board: [
["", "", ""],
["", "", ""],
["", "", ""],
],
currentPlayer: "X",
winner: null,
});
};
return (
<div className="tic-tac-toe">
<div className="board">
{game.board.map((row, i) =>
row.map((cell, j) => (
<button
key={`${i}-${j}`}
onClick={() => handleClick(i, j)}
disabled={!!cell || !!game.winner}
>
{cell}
</button>
))
)}
</div>
{game.winner ? (
<p>Winner: {game.winner}!</p>
) : (
<p>Current player: {game.currentPlayer}</p>
)}
<button onClick={resetGame}>Reset</button>
</div>
);
}
Nullable State
Copy
Ask AI
import { useWidgetState } from "skybridge/web";
type UserProfile = {
id: string;
name: string;
avatar: string;
};
function ProfileWidget() {
// State can be null - useful for optional data
const [profile, setProfile] = useWidgetState<UserProfile>(null);
if (!profile) {
return (
<div>
<p>No profile set</p>
<button
onClick={() =>
setProfile({
id: "1",
name: "John Doe",
avatar: "/avatars/default.png",
})
}
>
Create Profile
</button>
</div>
);
}
return (
<div className="profile">
<img src={profile.avatar} alt={profile.name} />
<h3>{profile.name}</h3>
<button onClick={() => setProfile(null)}>Clear Profile</button>
</div>
);
}