React frontends
A game ships two source files:
| Path | Holds |
|---|---|
/src/game.ts |
Required. gameConfig plus the game-logic hooks. The server runs these authoritatively. |
/src/frontend.tsx |
Required. The React client. Its single default export is a React component that renders the whole board. |
/src/game.ts holds the rules — pieces, spaces, applyActions, getAvailableActions, scoring, turn passing. /src/frontend.tsx holds everything the player sees. Both files exist from the moment the game is created; the starting template includes them.
The split is strict, and it is what keeps a game honest: the server is the sole authority over shared state. Your frontend never re-implements rules. It reads state and sends clicks; the server decides what those clicks mean, by running the same /src/game.ts you wrote.
The component contract
import { useMatch, useGameState, useAvailableActions } from "boardweaver/react";
export default function App() {
const match = useMatch();
const game = useGameState();
const available = useAvailableActions();
return (
<div>
{game.spaces("plot").map((space) => (
<button
key={space.spaceId}
onClick={() => match.click({ type: "Click", spaceId: space.spaceId })}
>
{space.spaceId}
</button>
))}
</div>
);
}
Hard requirements:
- A default export. No default export is a build error, caught at commit time.
- No props. The platform mounts your component; nothing passes it anything.
- Never mount anything yourself. The runtime owns the React root. Do not import
react-dom, do not callcreateRoot. - Do not import
reactfor rendering primitives you don't need.reactis available for hooks and types, but React itself is provided by the platform and version-locked — your bundle must not ship its own copy.
What is deliberately unavailable
Your frontend runs in a sandboxed frame with no network access, on an opaque origin, under a strict Content Security Policy. These are compile errors, and they would fail at runtime even if they weren't:
fetch,XMLHttpRequest,WebSocket,EventSource— the frame cannot talk to any server. All data arrives through the hooks.localStorage,sessionStorage,indexedDB,document.cookie— an opaque origin has no persistent storage.window,document, direct DOM access — use React. For frame size useuseViewport(), which is authoritative; there is nowindowworth measuring.
console and timers (setTimeout, setInterval, requestAnimationFrame) work normally.
This is not a restriction to work around — it is the containment boundary that lets untrusted game code run safely in a player's browser. If you find yourself needing one of these, the answer is almost always that the data belongs in game state, where the server owns it.
Assets and presentation
useImage(GameImage.Something)resolves a game image to a URL for<img src>. Image keys come from the same per-gameGameImageenum exported by"boardweaver"that/src/game.tsuses.useGameTheme()returns the game's validated/theme.jsondocument; pair it withuseColorMode()to pick the light or dark side of each color pair.useViewport()gives the frame's current size in CSS pixels and re-renders on change.
On a phone
Below 768 px wide, the frame fills the whole screen. Boardweaver shows no top bar, sidebar or action bar during a match. The one control it keeps is the Boardweaver mark: a 44 px round button, 12 px in from a corner of the frame, floating over your game. Players tap it to switch matches and drag it to any of the four corners. It starts in the top-left corner.
Keep controls a player must reach (buttons, a hand of cards) out of a 56 px square in each corner, or make sure the mark covering one of them never blocks a move.
Available modules
Only these imports resolve. Bare imports of anything else fail the build.
| Module | What it is |
|---|---|
boardweaver |
The same runtime the backend uses: GameImage, GameFont, defs, types. |
boardweaver/react |
The match API — every hook in this doc. See react-match-api. |
boardweaver/immer |
Version-locked Immer (produce, produceWithPatches, applyPatches, Patch) for client state. See react-client-state. |
boardweaver/dnd |
Drag and drop primitives. |
react, react/jsx-runtime |
Hooks and types. Provided by the platform. |
/src/** |
Your own tree, by relative or absolute path. |
Bundle size
The client bundle is /src/frontend.tsx and all of /src/game.ts, built together into one file that ships to every player on every match load. validate_code rejects it over 1024 KB, measured after minification.
game.ts is in there because the client runs your rules locally to predict the result of a click before the server answers. It is re-exported wholesale, so nothing in it is dropped for going unused by the UI: a large rules engine spends the budget whether or not your frontend touches it. There is currently no way to keep the rules server-only.
The bundle is minified, and comments never survive the build, so neither is worth trimming for size. What counts is executable code. Two things do not count at all:
- Platform modules.
react,boardweaver/react,boardweaver/dnd,boardweaver/immer,boardweaver/uiandboardweaver/motionare runtime externals resolved to the frame, not copies in your bundle. - Game images and fonts. Uploaded assets are fetched by URL. Inlining an image as a base64 string literal is the one case where an asset does land in the bundle, and it is the fastest way to blow the limit.
Next
react-match-api— every hook, the click lifecycle, and what re-renders when.react-client-state— selection, tentative moves, and undo.build-react-game-instructions— the end-to-end build workflow.v2-game-source— what/src/game.tsmust export, and the config object.v2-hooks— every hook's contract, and the action union.v2-entities—PieceDef/SpaceDefand the three state buckets.v2-game-state— theGameStateread/write API, shared withuseGameState().