Skip to main content
Import hooks from @1upvision/sdk:
import {
  useExtensionStorage,
  useExtensionContext,
  useVisionState,
} from "@1upvision/sdk";

useExtensionStorage

The main hook for reading and writing data. Storage is persisted on the server and syncs in real-time between all targets (editor, layer, interactive page).
const [storage, setStorage] = useExtensionStorage();
ValueTypeDescription
storageRecord<string, unknown>The current stored data
setStorage(data: Record<string, unknown>) => voidReplace the entire storage object

Basic Usage

import {
  Vision,
  CompactView,
  Toggle,
  useExtensionStorage,
} from "@1upvision/sdk";

function Settings() {
  const [storage, setStorage] = useExtensionStorage();

  return (
    <CompactView title="Settings">
      <Toggle
        label="Enabled"
        checked={storage.enabled ?? false}
        onChange={(checked) => setStorage({ ...storage, enabled: checked })}
      />
    </CompactView>
  );
}

Vision.render(<Settings />, { target: "editor" });

Real-Time Sync

When you call setStorage in any target, all other targets update automatically:
  1. Streamer toggles a switch in the editor panel
  2. The value is persisted to the server
  3. The OBS layer receives the update and re-renders
No websocket setup, no polling, no extra code. It just works.
setStorage replaces the entire storage object. Always spread the existing storage when updating a single key:
// Correct — keeps existing data
setStorage({ ...storage, myKey: newValue });

// Wrong — erases everything except myKey
setStorage({ myKey: newValue });

useVisionState

useVisionState is a key-scoped state hook backed by extension storage. Use it when you want useState-style ergonomics without manually spreading the entire storage object on every update.
const [count, setCount] = useVisionState("count", 0);
ValueTypeDescription
valueTCurrent value for the given key
setValue(next: T | (prev: T) => T) => voidUpdate only this key in shared storage

Example

import {
  Vision,
  CompactView,
  Text,
  Button,
  useVisionState,
} from "@1upvision/sdk";

function Counter() {
  const [count, setCount] = useVisionState("count", 0);

  return (
    <CompactView title="Counter">
      <Text content={`Count: ${count}`} />
      <Button label="Increment" onClick={() => setCount((prev) => prev + 1)} />
    </CompactView>
  );
}

Vision.render(<Counter />, { target: "editor" });

When to use which

  • useExtensionStorage: when you want to read/write the whole storage object
  • useVisionState: when you want one key with useState-style updates

useExtensionContext

Returns metadata about the running extension instance. Useful for rendering different UIs per target or for debugging.
const context = useExtensionContext();
Returns null until the extension is initialized, then:
PropertyTypeDescription
extensionIdstringUnique ID of this extension
target"editor" | "layer" | "interactive"Which target is running
overlayIdstringThe overlay this extension belongs to
interactiveUrlstring?Absolute URL to this extension’s interactive page
const context = useExtensionContext();

let joinUrl = "";
if (context?.interactiveUrl) {
  const url = new URL(context.interactiveUrl);
  url.searchParams.set("player", "1");
  joinUrl = url.toString();
}

Render Different UI per Target

import {
  Vision,
  CompactView,
  Text,
  Button,
  useExtensionContext,
  useExtensionStorage,
} from "@1upvision/sdk";

function MyExtension() {
  const context = useExtensionContext();
  const [storage, setStorage] = useExtensionStorage();

  if (!context) return null;

  if (context.target === "editor") {
    return (
      <CompactView title="Settings">
        <Text content="Configure your extension here." variant="muted" />
      </CompactView>
    );
  }

  if (context.target === "interactive") {
    return (
      <CompactView title="Controls">
        <Button
          label="Trigger Effect"
          onClick={() => setStorage({ ...storage, trigger: Date.now() })}
        />
      </CompactView>
    );
  }

  // Layer target — display only
  return <Text content={storage.message ?? "Hello!"} variant="heading" />;
}

Vision.render(<MyExtension />);
If your targets share very little UI, it’s usually cleaner to use separate entry point files (admin.tsx, layer.tsx, interactive.tsx) instead of branching with useExtensionContext.

useQuery and useMutation

If your extension uses server functions, two additional hooks are available for calling them from the client:
import { useQuery, useMutation } from "@1upvision/sdk";

useQuery

Calls a server query function by name. Returns the data, loading state, and error.
const { data, isLoading, error, refetch } = useQuery("getPlayers");
const { data: player } = useQuery("getPlayerById", { id: "abc123" });
ValueTypeDescription
dataT | undefinedThe query result
isLoadingbooleanWhether the query is in progress
errorError | nullError if the query failed
refetch() => voidManually re-run the query

useMutation

Calls a server mutation or action function by name. Automatically invalidates active queries on success.
const { mutate, mutateAsync, isLoading, error } = useMutation("addPlayer");

// Fire-and-forget
mutate({ name: "Alice", score: 100 });

// Async with error handling
const result = await mutateAsync({ name: "Bob", score: 200 });
ValueTypeDescription
mutate(args?: Record<string, unknown>) => voidFire-and-forget invocation
mutateAsync(args?: Record<string, unknown>) => Promise<T>Async invocation with return value
dataT | undefinedThe last mutation result
isLoadingbooleanWhether the mutation is in progress
errorError | nullError if the mutation failed

Which hook for which function type

Server functionCall with
query(...) / queryRows(...)useQuery
mutation(...) / insertRow / patchRow / deleteRowuseMutation
action(...) / fetchAction(...)useMutation
Using the wrong hook causes a FUNCTION_NOT_FOUND error at runtime. For example, calling a mutation with useQuery will fail.
See Server Functions for full details on defining functions.