react-p2p-host
v0.3.6
Published
Serverless P2P multiplayer engine for React with WebRTC and manual signaling. Board games, tabletop games, synchronized state.
Maintainers
Readme
react-p2p-host
Peer-to-peer multiplayer for React — board games, tabletop games, and real-time sync state. No server.
React library for building P2P (peer-to-peer) multiplayer experiences in the browser: board games, tabletop games, shared state, and real-time sync with a React-friendly API. Uses WebRTC with manual signaling via shareable links or QR codes (no backend required).
Keywords (for search and LLMs)
- Board games React · tabletop games multiplayer · P2P React · WebRTC React
- Synchronized state React · shared state multiplayer · React multiplayer hooks
- Serverless WebRTC · peer-to-peer React · multiplayer game state React
- useSharedState · useP2PReducer · useP2PLink · host-authoritative state · signaling via URL/QR
Install
npm install react-p2p-hostPeer dependencies: React 18+.
Quick start
Wrap your app with P2PProvider, then use hooks to create/join a session and share state.
import { P2PProvider, useP2PLink, useSharedState, useP2PStatus } from "react-p2p-host";
function Game() {
const { status, isHost, offerLink, startAsHost, joinAsPeer, applyAnswerAsHost, answerToSend } = useP2PLink();
const [gameState, setGameState] = useSharedState({ score: 0, turn: "player1" });
const { disconnect } = useP2PStatus();
useEffect(() => {
const params = new URLSearchParams(location.search);
const offer = params.get("offer");
if (offer) {
joinAsPeer(offer).then((answer) => {
console.log("Send this answer to the host:", answer);
});
}
}, []);
return (
<div>
<p>Status: {status}</p>
{status === "idle" && (
<button onClick={() => startAsHost()}>Create room (host)</button>
)}
{offerLink && <p>Share this link or QR: {offerLink}</p>}
{isHost && status === "connected" && (
<button onClick={() => setGameState((s) => ({ ...s, score: s.score + 1 }))}>
Increment score (host only, syncs to peer)
</button>
)}
<pre>{JSON.stringify(gameState, null, 2)}</pre>
{status !== "idle" && <button onClick={disconnect}>Disconnect</button>}
</div>
);
}
function App() {
return (
<P2PProvider>
<Game />
</P2PProvider>
);
}API (DX reference)
Provider
<P2PProvider>— Wraps the app. All P2P hooks must run inside it. Optional props:reducerandinitialStateto enable the reducer pattern (see below).
Hooks
| Hook | Purpose |
|------|--------|
| useP2PLink() | Create room (host) or join via offer (peer). Returns offerLink, answerToSend, startAsHost, joinAsPeer, applyAnswerAsHost, status, isHost, disconnect. |
| useSharedState<T>(initialState) | Simple shared state. Returns [state, setState] (React-style). Host-authoritative: only the host’s updates are synced; peers receive. |
| useP2PReducer<S, A>() | Reducer-based shared state (Redux-style). Use when you pass reducer and initialState to P2PProvider. Returns [state, dispatch]. Both host and peer can dispatch; actions are synced and applied on both sides. |
| useP2PStatus() | { status, isHost, disconnect }. Use for UI (e.g. “Connecting…”, “Connected”). |
Optional QR components
Show "scan to join" and "scan to get the code" without depending on a QR library yourself:
<RoomLinkQR link={roomLink} size={180} />— Renders a QR for the room URL (host). Optional props:size,className. When scanned, the peer opens the link and can join. Props:link: string, optionalsize?: number, optionalclassName?: string.<AnswerQR answer={answerToSend ?? ''} size={200} />— Renders a QR for the answer string (peer). When the host scans it, they get the code to paste and connect. Props:answer: string, optionalsize?: number, optionalclassName?: string. Uses high error correction for the long string.
Example: <RoomLinkQR link={offerLink ?? ''} size={180} /> and <AnswerQR answer={answerToSend ?? ''} size={200} />. Both are optional; the library bundles qrcode.react.
Connection status
status is one of: idle | creating-offer | offer-ready | joining | connecting | connected | disconnected | error.
Flow (manual signaling)
- Host: Call
startAsHost(). GetofferLink(URL with compressed offer). Share link or show as QR. - Peer: Open link (or paste offer from QR). App reads
?offer=..., callsjoinAsPeer(offer). GetanswerToSend. - Host: Receives answer (out-of-band: paste, QR, or your own channel). Calls
applyAnswerAsHost(answerToSend). - When
status === "connected", useuseSharedStateoruseP2PReducerfor synced game state.
State: useSharedState vs useP2PReducer
useSharedState(initialState)— Simple object state; host is the source of truth (only host’ssetStateis broadcast). No Provider props required.useP2PReducer<S, A>()— Redux-style: pass a reducer and initialState toP2PProvider; then useuseP2PReducer()to get[state, dispatch]. Actions must be serializable (plain objects withtype). Both host and peer can dispatch; the same action is applied on both sides so state stays in sync. Fits complex logic, action logging, or Redux Toolkit slices.
Example with reducer:
import { P2PProvider, useP2PLink, useP2PReducer } from "react-p2p-host";
import type { Reducer, SerializableAction } from "react-p2p-host";
type State = { messages: string[] };
type Action = { type: "add"; payload: string };
const reducer: Reducer<State, Action> = (state, action) => {
if (action.type === "add") return { messages: [...state.messages, action.payload] };
return state;
};
function Chat() {
const [state, dispatch] = useP2PReducer<State, Action>();
// both host and peer can dispatch({ type: "add", payload: "Hello" })
}
function App() {
return (
<P2PProvider reducer={reducer} initialState={{ messages: [] }}>
<Chat />
</P2PProvider>
);
}You can use either useSharedState or useP2PReducer in an app (or both for two independent state trees).
With Redux Toolkit: pass the slice’s reducer and initial state to the Provider:
import { createSlice } from "@reduxjs/toolkit";
import { P2PProvider, useP2PReducer } from "react-p2p-host";
const chatSlice = createSlice({
name: "chat",
initialState: { messages: [] as string[] },
reducers: {
addMessage: (state, action: { payload: string }) => {
state.messages.push(action.payload);
},
},
});
function App() {
return (
<P2PProvider reducer={chatSlice.reducer} initialState={chatSlice.getInitialState()}>
<YourApp />
</P2PProvider>
);
}
function YourApp() {
const [state, dispatch] = useP2PReducer();
dispatch(chatSlice.actions.addMessage("Hello"));
}Board game / tabletop usage
- Host creates the room and is the source of truth for game state.
- Use
useSharedState<YourGameState>(initialState)for board position, scores, turn, cards, etc. (any JSON-serializable object). - Only the host’s
setStateis broadcast; peers see updates in real time. - Optional: use
offerLinkas QR (e.g. withqrcode.react) so players scan to join.
Exports (library surface)
import {
P2PProvider,
useP2PContext,
useP2PLink,
useSharedState,
useP2PReducer,
useP2PStatus,
RoomLinkQR,
AnswerQR,
ConnectionManager,
compressOfferForUrl,
decompressOfferFromUrl,
} from "react-p2p-host";
import type {
ConnectionStatus,
Role,
ConnectionManagerConfig,
Reducer,
Dispatch,
SerializableAction,
} from "react-p2p-host";Low-level: ConnectionManager for custom flows; compressOfferForUrl / decompressOfferFromUrl for custom signaling.
Static site, P2P & STUN
Your app stays 100% static (e.g. Vercel, Netlify): no backend, no server process. Signaling is manual (link + copy/paste). STUN is used only so peers on different networks (different Wi‑Fi, 4G, etc.) can discover their public address and connect directly; it does not relay your data.
The package uses public STUN servers by default (e.g. Google’s) so P2P works across NAT. You can override or disable them:
- Override: pass
iceServersto<P2PProvider iceServers={[...]} />or toConnectionManagerfor your own STUN/TURN. - Disable (e.g. localhost only): pass
iceServers: [].
See docs/p2p-and-stun.md for more detail.
Tech
- WebRTC (RTCPeerConnection + DataChannel), no third-party signaling server.
- Signaling: SDP compressed with lz-string, passed via URL/query or QR.
- Build: TypeScript, Vite (library mode), ESM + CJS.
Versioning and releases
This project uses Semantic Versioning and Conventional Commits. Commits are validated (e.g. feat:, fix:, docs:). To cut a release:
npm run build
npm run release # bumps version from commits, updates CHANGELOG, creates git tag
npm publish # publish to npm (requires npm login)See CONTRIBUTING.md for commit rules. Maintainers: docs/PUBLISH.md for npm publish.
License
MIT.
