Skip to main content
The 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

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

defaultState?: T | (() => T | null) | null
The default state to use if no persisted state exists. Can be:
  • A value of type T
  • A function that returns T or null (lazy initialization)
  • null or undefined

Type Parameters

T

T extends Record<string, unknown>
The type of your widget state. Must be a plain object.

Returns

[state: T | null, setState: (state: SetStateAction<T | null>) => void]
A tuple containing:

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

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

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

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

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

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>
  );
}