State and effects
Combine React hooks with Ludicord lifecycle, storage, queries, and game loops.
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.
1import { useState } from "react";23export 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.
1import { useEffect } from "react";23export 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.
1import { useActivityLifecycle } from "ludicord/activity";23export 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.
1const theme = useActivityStorage("theme", "dark", { scope: "user" });23return <button onClick={() => theme.setValue("light")}>{theme.value}</button>;
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.
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.
1const loop = useGameLoop(({ delta }) => world.step(delta), {2 pauseWhenHidden: true,3 maxDelta: 50,4});
#Choose the correct lifetime
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.