Step 5: Control the relay
Create dashboard/src/components/SessionButton.jsx. The button owns its own on/pending state, calls the state RPC, and emits onStart / onStop / onError so a parent component can run side effects like a session timer or an error toast. The highlighted lines are the RelayX integration. Everything else is state management and styling.
import { useState } from "react";
import { app, DEVICE_IDENT } from "../relayx.js";
import { I } from "../lib/icons.jsx";
export function SessionButton({ onStart, onStop, onError }) {
const [on, setOn] = useState(false);
const [pending, setPending] = useState(false);
async function toggle() {
const target = !on;
setPending(true);
try {
const result = await app.rpc.call({
device_ident: DEVICE_IDENT,
name: "state",
timeout: 10,
data: { on: target },
});
if (result?.status !== "ok") {
const msg =
result?.data?.message ||
result?.message ||
`Device returned status "${result?.status ?? "unknown"}"`;
throw new Error(msg);
}
const applied = Boolean(result?.data?.on ?? target);
setOn(applied);
if (applied) onStart?.();
else onStop?.();
} catch (err) {
onError?.(err instanceof Error ? err : new Error(String(err)));
} finally {
setPending(false);
}
}
return (
<button
onClick={toggle}
disabled={pending}
style={{
width: "100%",
padding: "12px 14px",
borderRadius: 10,
fontSize: 13,
fontWeight: 600,
background: on ? "var(--ink)" : "var(--green)",
color: "white",
border: `1px solid ${on ? "var(--ink)" : "var(--green)"}`,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
opacity: pending ? 0.7 : 1,
cursor: pending ? "not-allowed" : "pointer",
}}
>
{on ? <I.stop width="13" height="13" /> : <I.play width="13" height="13" />}
{pending ? (on ? "Stopping…" : "Starting…") : on ? "Stop session" : "Start session"}
</button>
);
}
The key parts of the RelayX integration:
app.rpc.call({ device_ident, name: "state", data: { on } })sends the RPC to the device.- The SDK resolves the promise with the device's response envelope. Treat anything other than
status === "ok"as a failure and surface the message. - The
onStart/onStopcallbacks only fire after the RPC succeeds, so the parent's derived state stays in sync with the real relay state.
The icon and color values (I.play, I.stop, var(--ink), var(--green)) come from the project's design system. Adapt them to your own palette, or see the reference repo for the icon set and CSS variables.
Test it
Render the button somewhere and wire up the callbacks. A minimal check inside App.jsx:
import { SessionButton } from "./components/SessionButton.jsx";
<SessionButton
onStart={() => console.log("Session started")}
onStop={() => console.log("Session stopped")}
onError={(err) => alert(err.message)}
/>
Reload the dashboard and press Start session. The relay should click shut within 500 ms. The current chart begins to climb. Press Stop session. The relay opens and the readings fall back to zero. Check the browser console for the callback logs.
If nothing happens, check the browser console for the RPC error. Common failure modes are a mismatched device ident, an offline device, or an invalid payload rejected by the device's rpc_set_state handler.