Bridge Protocol v1
This page is the source of truth for the wire protocol between the UnrealReactBridge UE5 plugin and any web client. You do not need the unreal-react-bridge npm SDK — any web framework (Vue, Svelte, plain JS, htmx, …) can implement this protocol directly.
| Plugin version | 1.x |
| Protocol version | 1 |
| SDK version | 1.x |
Transport mechanism
The plugin uses SWebBrowser::BindUObject (UE5 5.7+ official API) to expose a C++ object at window.ue.bridge in the browser context. UE5 automatically lowercases all method names, so OnReactEvent on the C++ side is reachable as window.ue.bridge.onreactevent in JavaScript.
Lifecycle
1. Developer calls LoadURL (or sets URL in WBP_WebHUD Details panel)
2. Page starts loading → UE calls BindUObject("bridge", component)
→ window.ue.bridge becomes available
3. Page finishes loading → UE injects init script:
window.dispatchEvent(new CustomEvent('ue5-bridge-ready',
{ detail: { version: '1', initialState: { ... } } }))
4. Web side receives 'ue5-bridge-ready' → calls window.ue.bridge.onreactready()
5. UE OnReactReady() fires → flushes queued events
6. OnReactReadyEvent BP delegate broadcasts → bridge is liveOn page reload or another LoadURL() call, the lifecycle restarts from step 1.
See /guide/lifecycle for a per-step walkthrough.
Web → UE
window.ue.bridge.onreactevent(eventName, jsonData)| Param | Type | Description |
|---|---|---|
eventName | string | Colon-separated event name, e.g. "ui:reload:click" |
jsonData | string | A valid JSON string, e.g. '{"weapon":"pistol","ammo":15}' |
The plugin parses jsonData and forwards it to any UReactBridgeReceiverComponent bound to the matching event name, and to the Subsystem-level OnEventReceived delegate.
UE → Web
UE dispatches browser CustomEvents on window. The payload is always in event.detail as a parsed JavaScript object.
window.addEventListener('player:health:update', (event) => {
const { value, max } = event.detail;
updateHealthBar(value, max);
});The payload is transmitted as base64-encoded UTF-8 JSON and decoded by the init script before dispatch. Your listener always receives a plain JS object — no manual decoding needed.
Signaling readiness
The web side must call onreactready() to start event delivery. Events sent by UE before readiness are queued (capacity governed by Max Event Queue Size — see /reference/settings) and flushed on receipt of the ready signal.
Cover both paths — bridge already present (page reload) and bridge arriving later (cold load):
// Cold-load path: listen for bridge init
window.addEventListener('ue5-bridge-ready', () => {
window.ue.bridge.onreactready();
});
// Reload path: bridge already on window
if (window.ue?.bridge?.onreactready) {
window.ue.bridge.onreactready();
}Event naming convention
Recommended: domain:entity:action.
| Direction | Example | Meaning |
|---|---|---|
| UE → Web | player:health:update | UE sends updated health value |
| UE → Web | game:state:change | UE sends new game state |
| Web → UE | ui:reload:click | User clicked reload button |
| Web → UE | ui:menu:open | User opened menu |
Typed payload conventions
When a payload originates from UReactBridgeSenderComponent's typed Push* helpers (or the equivalent C++ template PushStruct<T>), the wire shape is always wrapped under value:
{ "value": <T> }| Send-side call | event.detail shape |
|---|---|
PushFloat("player:health", 75.0f) | { "value": 75 } |
PushInt("player:score", 42) | { "value": 42 } |
PushString("player:name", "alice") | { "value": "alice" } |
PushBool("ui:visible", true) | { "value": true } |
PushVector("player:loc", FVector(1,2,3)) | { "value": { "X": 1, "Y": 2, "Z": 3 } } |
PushStruct("player:state", FPlayerState{...}) | { "value": <FJsonObjectConverter output> } |
PushMap("inventory:slot", { {"id","sword"}, {"qty","1"} }) | { "id": "sword", "qty": "1" } (flat, no envelope) |
PushRaw("player:hp", "{\"value\":720,\"max\":900}") | { "value": 720, "max": 900 } (verbatim) |
Two carve-outs:
PushMapemits the map as a flat object ({ "k1": "v1", ... }) — novaluewrap (the map IS already an object). Values are all strings (the BPTMap<FString, FString>shape). Key order on the wire is non-deterministic — read by name, not by position.PushRawis a verbatim passthrough — no shape rule. Use it when you need numeric / nested / array payloads that bypass the envelope.
Subsystem-level BroadcastEvent / SendEvent and the BP-level WebHUD::Send API also pass the JSON payload through unmodified — the { "value": <X> } convention is specific to the typed sender helpers.
useUnrealState unwrap rule
useUnrealState<T>(name, initial) auto-unwraps the envelope only when the payload is an object with exactly one own key named value — i.e. the shape produced by the scalar helpers above. Payloads that pass through PushMap / PushRaw / Subsystem broadcast (objects with multiple keys, arrays, or primitives) reach the hook untouched, so e.g. { "value": 720, "max": 900 } is delivered as the full object rather than being silently collapsed to 720.
See /reference/react-hooks#useunrealstate.
Initial state
The ue5-bridge-ready event's detail always includes an initialState field — an object of arbitrary JSON-serializable key/value pairs the host UE side wants the web app to read on first paint.
window.addEventListener('ue5-bridge-ready', (event) => {
const { version, initialState } = event.detail;
console.log('player snapshot', initialState.player);
});initialState is always present (empty object {} if the host provided nothing). v1 consumers that read only detail.version keep working unchanged.
Mapping rule on the UE side: each (key, jsonValue) pair in the host's InitialState map becomes key: <parsed JSON> if jsonValue is valid JSON; otherwise it becomes key: "<jsonValue>" (wrapped as string). BP authors can pass either structured JSON ({"hp":100}) or plain strings ("hello") — the latter just shows up as a JSON string on the web side.
See /guide/initial-state.
Layer registry note
The Add Web HUD Blueprint node creates UWebHUDLayer widgets that register with the same UUnrealReactBridgeSubsystem HUD-widget registry as legacy WBP_WebHUD widgets. The Name you pass to Add Web HUD becomes the BridgeName; Sender component routing, Subsystem SendEvent / BroadcastEvent, and the wire protocol itself behave identically for both kinds of widget. Mixing legacy widgets and new layers in the same project is supported.
Minimal vanilla JS implementation
<script>
// Receive events from UE
window.addEventListener('player:health:update', (e) => {
document.getElementById('health').textContent = e.detail.value;
});
// Send events to UE
function sendToUE(name, data) {
if (window.ue?.bridge?.onreactevent) {
window.ue.bridge.onreactevent(name, JSON.stringify(data));
}
}
// Signal readiness (both paths)
function signalReady() {
if (window.ue?.bridge?.onreactready) window.ue.bridge.onreactready();
}
window.addEventListener('ue5-bridge-ready', signalReady);
signalReady(); // in case bridge already exists (reload)
</script>Version compatibility
| Plugin Version | Protocol Version | SDK Version |
|---|---|---|
| 1.x | 1 | 1.x |
Breaking changes to the protocol increment the major version. The ue5-bridge-ready event detail includes { version: '1' } so the web side can detect mismatches.
See also
/guide/lifecycle— step-by-step lifecycle walkthrough./reference/react-hooks#useunrealstate— the hook that implements the unwrap rule.