Skip to content

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 version1.x
Protocol version1
SDK version1.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

text
1. Developer calls LoadReactURL (or sets URL in the ReactWebBrowser 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 live

On page reload or another LoadReactURL() call, the lifecycle restarts from step 1.

See /guide/lifecycle for a per-step walkthrough.

Web → UE

js
window.ue.bridge.onreactevent(eventName, jsonData)
ParamTypeDescription
eventNamestringColon-separated event name, e.g. "ui:reload:click"
jsonDatastringA 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.

js
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):

js
// 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.

DirectionExampleMeaning
UE → Webplayer:health:updateUE sends updated health value
UE → Webgame:state:changeUE sends new game state
Web → UEui:reload:clickUser clicked reload button
Web → UEui:menu:openUser 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:

json
{ "value": <T> }
Send-side callevent.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:

  • PushMap emits the map as a flat object ({ "k1": "v1", ... }) — no value wrap (the map IS already an object). Values are all strings (the BP TMap<FString, FString> shape). Key order on the wire is non-deterministic — read by name, not by position.
  • PushRaw is 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 browser's own Send Event node 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.

js
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.

Browser registry note

Every UReactWebBrowser self-registers with the UUnrealReactBridgeSubsystem when its Slate widget is built, and unregisters when it is released. That registry is what BroadcastEvent fans out over and what SendEvent(BridgeName, …) resolves against — so a browser is addressable for exactly as long as it is on a live widget tree, with no separate mount call to keep in sync.

Minimal vanilla JS implementation

html
<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 VersionProtocol VersionSDK Version
1.x11.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

Released under the MIT License.