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();
| Value | Type | Description |
|---|
storage | Record<string, unknown> | The current stored data |
setStorage | (data: Record<string, unknown>) => void | Replace 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:
- Streamer toggles a switch in the editor panel
- The value is persisted to the server
- 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);
| Value | Type | Description |
|---|
value | T | Current value for the given key |
setValue | (next: T | (prev: T) => T) => void | Update 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:
| Property | Type | Description |
|---|
extensionId | string | Unique ID of this extension |
target | "editor" | "layer" | "interactive" | Which target is running |
overlayId | string | The overlay this extension belongs to |
interactiveUrl | string? | Absolute URL to this extension’s interactive page |
Build an Interactive Link
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" });
| Value | Type | Description |
|---|
data | T | undefined | The query result |
isLoading | boolean | Whether the query is in progress |
error | Error | null | Error if the query failed |
refetch | () => void | Manually 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 });
| Value | Type | Description |
|---|
mutate | (args?: Record<string, unknown>) => void | Fire-and-forget invocation |
mutateAsync | (args?: Record<string, unknown>) => Promise<T> | Async invocation with return value |
data | T | undefined | The last mutation result |
isLoading | boolean | Whether the mutation is in progress |
error | Error | null | Error if the mutation failed |
Which hook for which function type
| Server function | Call with |
|---|
query(...) / queryRows(...) | useQuery |
mutation(...) / insertRow / patchRow / deleteRow | useMutation |
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.