Ludicord
Guide 15 · Full stack

Shared Activity state

Synchronize optimistic state across everyone in one Discord Activity instance.

Ludicord 3.1.0Public source 20c4889
State convergenceEvery client lands on the same server revision
Framework managed
CHSA
revision 42state.patchvalidated once
YUNO

Shared Activity state combines a WebSocket route with an optimistic React state hook. Values are scoped to one verified Discord Activity instance and protected by server validation, size limits, revisions, and room cleanup.

#Define the server state

app/ws/game/route.tsts
1import { defineSharedActivityState } from "ludicord/ws/server";
2
3interface GameState {
4 round: number;
5 score: number;
6}
7
8function isGameState(value: unknown): value is GameState {
9 if (typeof value !== "object" || value === null) return false;
10 const game = value as Partial<GameState>;
11 return Number.isSafeInteger(game.round) && Number.isSafeInteger(game.score);
12}
13
14export default defineSharedActivityState<GameState>({
15 initialState: { round: 1, score: 0 },
16 validate: isGameState,
17});

The server refuses values that fail validation, cannot be serialized as JSON, or exceed the configured byte limit.

#Use the state in React

app/embeds/game/embed.tsxtsx
1import { useSharedActivityState } from "ludicord/activity";
2
3export default function embed() {
4 const game = useSharedActivityState("/ws/game", "main", { round: 1, score: 0 });
5
6 return (
7 <button disabled={game.status !== "open"} onClick={() => game.setValue((value) => ({ ...value, score: value.score + 1 }))}>
8 Score {game.value.score} · revision {game.revision}
9 </button>
10 );
11}

setValue has the same value-or-updater shape as React state. It updates the local UI immediately, then sends the value using the last acknowledged revision.

#Understand synchronization

FieldMeaning
valueCurrent optimistic or server-confirmed value
revisionLatest confirmed server revision
synchronizedWhether local value matches a confirmed server snapshot
statusUnderlying WebSocket connection status
setValueOptimistically writes the next value

If two clients write from the same old revision, the server sends the current value to the stale client. The hook reconciles and flushes a newer pending local value when one exists.

#Use multiple keys

The route stores independent values by key inside the Activity room.

tsxtsx
1const board = useSharedActivityState("/ws/game", "board", emptyBoard);
2const timer = useSharedActivityState("/ws/game", "timer", 60);

Keys must contain 1–128 characters. Keep them stable; changing the key subscribes the hook to a different value.

#Configure room safety

tsts
1export default defineSharedActivityState({
2 initialState: ({ key, client }) => createInitialState(key, client.ludicord.user.id),
3 validate: isGameState,
4 maxRooms: 1_000,
5 roomTtl: 60 * 60 * 1_000,
6 maxValueBytes: 64 * 1024,
7});
OptionPurpose
maxRoomsBounds process memory used by active instances
roomTtlRemoves rooms that have not been touched
maxValueBytesPrevents oversized serialized values
validateEnforces the server-owned state shape

The built-in store is process memory. For state that must survive deployment or synchronize across multiple server processes, persist authoritative data in a shared database and use WebSockets as the live transport.

#Choose shared state deliberately

Use React useState for one player's temporary UI, useActivityStorage for browser-persisted preferences, useActivityQuery for server data, and shared Activity state for small values everyone in one live instance needs now.

#Next step

Continue to Data fetching for cached HTTP data that has a different lifetime from a live WebSocket value.