Architecture

OneJS connects JavaScript to Unity through several layers.

Architecture Overview

┌─────────────────────────────────────────────────┐
│                  Your React Code                 │
├─────────────────────────────────────────────────┤
│              onejs-react Reconciler              │
├─────────────────────────────────────────────────┤
│           QuickJS JavaScript Engine              │
├─────────────────────────────────────────────────┤
│     QuickJSUIBridge (Events, Scheduling)         │
├─────────────────────────────────────────────────┤
│              UI Toolkit Elements                 │
└─────────────────────────────────────────────────┘

The JavaScript Engine

OneJS uses QuickJS, a small embeddable JavaScript engine. It runs your code in an isolated context with:

  • Full ES2020 support
  • Async/await and Promises
  • Modules (ESM)
QuickJS is interpreted, not JIT-compiled, so it works on iOS and other AOT/IL2CPP platforms where JIT is prohibited. For WebGL builds, OneJS uses the browser's native JavaScript engine instead.

The React Reconciler

onejs-react is a custom React reconciler. When you write:

<View style={{ padding: 20 }}>
    <Label text="Hello" />
</View>

The reconciler:

  1. Creates a VisualElement for the View
  2. Creates a Label element
  3. Sets the style properties
  4. Adds the Label as a child of the View
React's diffing algorithm handles updates efficiently. Only changed properties are updated.

Component Mapping

React ComponentUI Toolkit Element
<View>VisualElement
<Text>TextElement
<Label>Label
<Button>Button
<TextField>TextField
<Toggle>Toggle
<Slider>Slider
<ScrollView>ScrollView
<Image>Image
<ListView>ListView
Raw textTextElement
Text handling: Raw text children (e.g., <View>Hello</View>) automatically create TextElement instances. Use <Text> for primary text display and <Label> for form labels.

C# Interop

The CS global proxy uses reflection to access C# types:

CS.UnityEngine.Debug.Log("Hello")

This resolves to:

  1. Look up the UnityEngine.Debug type
  2. Find the Log method
  3. Convert arguments to C# types
  4. Invoke the method
  5. Convert the return value back to JavaScript
Objects returned from C# are wrapped in proxies with integer handles. Property access and method calls go through the interop layer.

Event System

UI Toolkit events are captured and routed to JavaScript:

  1. C# registers a TrickleDown callback on the root element
  2. When an event fires, it's serialized to JSON
  3. __dispatchEvent(handle, eventType, data) is called
  4. JavaScript looks up handlers and invokes them
UI Event → C# Capture → JSON Serialize → JS Dispatch → Your Handler

Scheduling

OneJS provides familiar timing APIs:

  • requestAnimationFrame(): Called every Unity Update
  • setTimeout() / setInterval(): Timer queue processed each frame
  • queueMicrotask(): For Promise resolution
The bridge's Tick() method drives all scheduling from Unity's Update loop.

Memory Management

C# objects referenced from JavaScript are tracked with integer handles:

  1. C# object → Handle registered in table
  2. JavaScript proxy wraps the handle
  3. When JS object is garbage collected → FinalizationRegistry releases handle
  4. C# object becomes eligible for GC
Manual cleanup is available via releaseObject() but rarely needed.

WebGL Differences

On WebGL, JavaScript runs in the browser's V8/SpiderMonkey engine:

  • JIT compilation (faster execution)
  • Browser's native RAF loop
  • No QuickJS overhead
The same code runs on both platforms. Only the underlying engine changes.