Ludicord
Guide 08 · Build the UI

State and effects

Combine React hooks with Ludicord lifecycle, storage, queries, and game loops.

Ludicord 3.1.0Public source 20c4889

Ludicord components are React components. Use normal React hooks for component behavior, then use Ludicord hooks when state must understand Discord identity, Activity lifecycle, persistence, data caching, animation, or multiplayer synchronization.

#Use React state for local UI

useState is the right choice when a value belongs to one mounted component and does not need to survive reloads or synchronize with another player.

app/embeds/home/embed.tsxtsx
1import { useState } from "react";
2
3export default function embed() {
4 const [open, setOpen] = useState(false);
5 return <button onClick={() => setOpen((value) => !value)}>{open ? "Close" : "Open"}</button>;
6}

Use useReducer for related transitions, useMemo for expensive derived values, useRef for mutable values that should not render, and Context for state shared through a React subtree.

#Use effects for external synchronization

useEffect connects a component to something outside React: a browser event, SDK subscription, timer, or imperative library. It should return cleanup whenever it creates a subscription.

components/visibility-log.tsxtsx
1import { useEffect } from "react";
2
3export function VisibilityLog() {
4 useEffect(() => {
5 const report = () => console.log(document.visibilityState);
6 document.addEventListener("visibilitychange", report);
7 return () => document.removeEventListener("visibilitychange", report);
8 }, []);
9 return null;
10}

Do not use an effect to calculate a value that can be computed during render. React Strict Mode may run development setup and cleanup more than once to reveal unsafe effects.

#Track the Activity lifecycle

useActivityLifecycle() combines browser visibility, focus, network status, Discord readiness, and WebSocket readiness.

components/connection-status.tsxtsx
1import { useActivityLifecycle } from "ludicord/activity";
2
3export function ConnectionStatus() {
4 const lifecycle = useActivityLifecycle();
5 return <span>{lifecycle.discordReady && lifecycle.online ? "Ready" : "Connecting"}</span>;
6}

This avoids duplicating window listeners in every feature.

#Persist state by scope

useActivityStorage() behaves like state while persisting to browser storage. Select the scope that owns the value.

app/embeds/settings/embed.tsxtsx
1const theme = useActivityStorage("theme", "dark", { scope: "user" });
2
3return <button onClick={() => theme.setValue("light")}>{theme.value}</button>;
ScopeStorage identity
activityThis application in this browser
userCurrent Discord user
guildCurrent Discord guild
channelCurrent Discord channel

User, guild, and channel storage remains unavailable until the required identity exists.

#Fetch remote data

useActivityQuery() adds shared caching, deduplicated in-flight work, retries, timeouts, stale times, manual refresh, and optimistic cache mutation.

app/embeds/profile/embed.tsxtsx
1const profile = useActivityQuery(
2 ["profile", user?.id],
3 async ({ signal }) => fetch("/api/profile", { signal }).then((response) => response.json()),
4 { enabled: Boolean(user), staleTime: 30_000, retries: 2 },
5);

#Run animation safely

useGameLoop() owns requestAnimationFrame, clamps unusually large frame deltas, and pauses while the document is hidden by default.

components/game-canvas.tsxtsx
1const loop = useGameLoop(({ delta }) => world.step(delta), {
2 pauseWhenHidden: true,
3 maxDelta: 50,
4});

#Choose the correct lifetime

NeedUse
Temporary component UIuseState or useReducer
Side effect or subscriptionuseEffect
Saved browser preferenceuseActivityStorage
Cached HTTP/server datauseActivityQuery
Per-frame game updateuseGameLoop
Discord presence summaryuseActivityPresence
State shared by all playersuseSharedActivityState

All hooks still follow the Rules of Hooks: call them unconditionally at the top level of a React component or custom hook.

#Next step

Open Authentication to understand which identity data client hooks may read and which checks must remain on the server.