Client SDK
What plugin UI code can reach in the browser.
@sharkord/plugin-sdk/client is the browser half of the SDK. It gives your components four things: Sharkord's own state, your server actions, messages your server pushed, and per-user storage.
import {
actions,
createCallAction,
useCanUseAction,
useCanUseCommand,
usePush,
useStoreSelector,
useUserData,
} from "@sharkord/plugin-sdk/client";The module reads window.__SHARKORD_STORE__ when it loads, so it only works inside a plugin bundle Sharkord served.
Reading State
Read the store for what the whole app knows. For what a component's own position implies, use the props its slot passes: selectedChannelId is the wrong answer inside a thread, and nothing here says which message or member a row belongs to.
import { useStoreSelector } from "@sharkord/plugin-sdk/client";
const SelectedChannel = () => {
const channelId = useStoreSelector((state) => state.selectedChannelId);
return <span>Selected channel: {channelId ?? "none"}</span>;
};The selector runs against this state:
| Field | Type |
|---|---|
users | public users on the server |
channels | channels |
categories | channel categories |
roles | roles |
emojis | custom emojis |
plugins | loaded plugins with UI |
ownUserId | number | undefined |
selectedChannelId | number | undefined |
currentVoiceChannelId | number | undefined |
publicSettings | the server's public settings |
The selector must return the same reference for unchanged state. Reading a
field ((state) => state.users) is fine; building a new array or object on
every call re-renders forever. Memoize those with createSelector, which the
host exposes on window.__SHARKORD_EXPOSED_LIBS__.
Calling the Server
import { createCallAction } from "@sharkord/plugin-sdk/client";
import type { TPlugin } from "../types";
const callAction = createCallAction<TPlugin>();
const SumButton = () => {
const onClick = async () => {
const result = await callAction("sum", { a: 1, b: 2 });
};
return <button onClick={onClick}>Calculate</button>;
};Call createCallAction once at module scope. The contract checks the action name, the payload, and the response. See Actions.
The channel the user has open is sent along with every call, so the server hands your action an invoker.channelId without you passing one. It is checked against that user's channel permissions before your handler runs.
Receiving Pushes
The server can send your clients anything at any time, without them asking:
// server
ctx.push.toUser(userId, { kind: "rolled", value: 6 });
ctx.push.toUsers([1, 2, 3], data);
ctx.push.toAll(data);// client
usePush<TPlugin>((data) => setLast(data));toAll reaches every user currently online. A push cannot exceed 64 KB of JSON, and there is no delivery guarantee: a user who is offline, or who connects a second later, does not get it. Use it for live updates, not for state that has to arrive.
Per-User Storage
useUserData is the browser side of ctx.userData: one JSON object per user per plugin, stored on the server.
import { useUserData } from "@sharkord/plugin-sdk/client";
const Preferences = () => {
const { data, loading, save } = useUserData<TPlugin>();
if (loading) return null;
return (
<button onClick={() => save({ ...data, compact: !data.compact })}>
Toggle
</button>
);
};A user can only read and write their own data, and the object is capped at 64 KB of JSON. Saving is rate limited like a command or action, and only works while the plugin is enabled, so a client cannot write rows for a plugin the server has never heard of. The server half can read and write anyone's with ctx.userData.get / set / delete. All of it is deleted when the plugin is removed. Since a user can write whatever they like here, treat it as user input on the server. See Data.
Checking Access
An admin can restrict any of your commands and actions to roles, so a button that calls one may be useless for the user looking at it. These two answer whether they may:
import { useCanUseAction } from "@sharkord/plugin-sdk/client";
import type { TPlugin } from "../types";
const RollButton = () => {
const canRoll = useCanUseAction<TPlugin>("roll");
return (
<button disabled={!canRoll} onClick={onRoll}>
Roll
</button>
);
};useCanUseCommand is the same for a slash command, for UI that lists or offers them. Both fold in USE_PLUGINS, so a user without it gets false either way, and both take the contract to autocomplete the name.
It exists so you can disable a button instead of letting the call fail. The server checks again on every call, and that answer is the one that counts.
Store Actions
Besides executePluginAction, which is what callAction uses, actions exposes what your UI can do directly as the signed-in user:
import { actions } from "@sharkord/plugin-sdk/client";
await actions.sendMessage(channelId, "hello");
actions.selectChannel(channelId);
const response = await actions.fetchPluginRoute("my-plugin", "/preferences");fetchPluginRoute attaches the user's token, so it is how you call your own authenticated HTTP routes without handling tokens yourself. These run as the current user, with that user's permissions.
Host Globals
The store and the shared libraries reach your bundle through globals, which is why your plugin does not ship its own React:
window.__SHARKORD_STORE__:getState,subscribe,actions,hookswindow.__SHARKORD_EXPOSED_LIBS__:createSelector,createCachedSelectorwindow.__SHARKORD_REACT__,__SHARKORD_REACT_JSX__,__SHARKORD_REACT_JSX_DEV__,__SHARKORD_REACT_DOM__,__SHARKORD_REACT_DOM_CLIENT__
The build tooling wires these up for you. Reach for them directly only when the SDK's helpers are not enough.
Reminder
Client code is public and runs in every user's browser. Do not put secrets in it, and do not treat a check you made here as security. Enforce it in a server action.