React 19 reconciler for Unity's UI Toolkit.
| File | Purpose |
|---|---|
src/host-config.ts |
React reconciler implementation (createInstance, commitUpdate, etc.) |
src/renderer.ts |
Entry point: render(element, container) |
src/components.tsx |
Component wrappers: View, Text, Label, Button, TextField, etc. |
src/screen.tsx |
Responsive design: ScreenProvider, useBreakpoint, useScreenSize, useResponsive |
src/types.ts |
TypeScript type definitions (includes Vector Drawing types) |
src/index.ts |
Package exports |
| Component | UI Toolkit Element | Description |
|---|---|---|
View |
VisualElement | Container element |
Text |
TextElement | Primary text display |
Label |
Label | Form labels, semantic labeling |
Button |
Button | Interactive button |
TextField |
TextField | Text input |
Toggle |
Toggle | Checkbox/toggle |
Slider |
Slider | Numeric slider |
ScrollView |
ScrollView | Scrollable container |
Image |
Image | Image display |
ListView |
ListView | Virtualized list |
Raw text in JSX (e.g., <View>Hello</View>) creates a TextElement, providing semantic distinction from explicit <Label> components.
import { render, View, Text, Label, Button } from 'onejs-react';
function App() {
return (
<View style={{ padding: 20 }}>
<Text text="Welcome!" style={{ fontSize: 24 }} />
<Button text="Click me" onClick={() => console.log('clicked')} />
<View>Raw text also works</View>
</View>
);
}
render(<App />, __root);OneJS has multiple type sources. Here's when to use each:
Import types from onejs-react for refs and component props:
import { View, Button, VisualElement, ButtonElement } from "onejs-react"
function MyComponent() {
const viewRef = useRef<VisualElement>(null)
const buttonRef = useRef<ButtonElement>(null)
useEffect(() => {
buttonRef.current?.Focus()
}, [])
return (
<View ref={viewRef}>
<Button ref={buttonRef} text="Click me" />
</View>
)
}For creating elements outside React, use unity-types:
import { Button } from "UnityEngine.UIElements"
const btn = new Button()
btn.text = "Dynamic Button"
__root.Add(btn)The render() function accepts any RenderContainer:
import { render, RenderContainer } from "onejs-react"
// __root is provided by the runtime
render(<App />, __root)RenderContainer (minimal: __csHandle, __csType)
└── VisualElement (full API: style, hierarchy, events)
├── TextElement (+ text property)
│ ├── LabelElement
│ └── ButtonElement
├── TextFieldElement (+ value, isPasswordField, etc.)
├── ToggleElement (+ value: boolean)
├── SliderElement (+ value, lowValue, highValue)
└── ScrollViewElement (+ scrollOffset, ScrollTo)
- Element types: Use
ojs-prefix internally (e.g.,ojs-view,ojs-button) to avoid conflicts with HTML types - Style shorthands:
padding/marginare expanded to individual properties (UI Toolkit requirement) - Style cleanup: When props change, removed style properties are cleared (not just new ones applied)
- className updates: Selective add/remove of classes (not full clear + reapply)
- Event handlers: Registered via
__eventAPIfrom QuickJSBootstrap.js - Instance structure:
{ element, type, props, eventHandlers: Map, appliedStyleKeys: Set }
npm run typecheck # TypeScript check (no build output - consumed directly by App)
npm test # Run test suite
npm run test:watch # Run tests in watch modeTest suite uses Vitest with mocked Unity CS globals. Tests are in src/__tests__/:
| File | Coverage |
|---|---|
host-config.test.ts |
Instance creation, style/className management, events, children |
renderer.test.tsx |
Integration tests: render(), unmount(), React state, effects |
components.test.tsx |
Component wrappers, prop passing, event mapping |
mocks.ts |
Mock implementations of Unity UI Toolkit classes |
setup.ts |
Global test setup for CS, __eventAPI |
OneJS exposes Unity's Painter2D API for GPU-accelerated vector graphics. Any element can render custom vector content via onGenerateVisualContent.
import { View, render } from "onejs-react"
function Circle() {
return (
<View
style={{ width: 200, height: 200, backgroundColor: "#333" }}
onGenerateVisualContent={(mgc) => {
const p = mgc.painter2D
// Draw a filled circle
p.fillColor = new CS.UnityEngine.Color(1, 0, 0, 1) // Red
p.BeginPath()
p.Arc(
new CS.UnityEngine.Vector2(100, 100), // center
80, // radius
CS.UnityEngine.UIElements.Angle.Degrees(0),
CS.UnityEngine.UIElements.Angle.Degrees(360),
CS.UnityEngine.UIElements.ArcDirection.Clockwise
)
p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
}}
/>
)
}Path operations:
BeginPath()- Start a new pathClosePath()- Close the current subpathMoveTo(point)- Move to point without drawingLineTo(point)- Draw line to pointArc(center, radius, startAngle, endAngle, direction)- Draw arcArcTo(p1, p2, radius)- Draw arc tangent to two linesBezierCurveTo(cp1, cp2, end)- Cubic bezier curveQuadraticCurveTo(cp, end)- Quadratic bezier curve
Rendering:
Fill(fillRule)- Fill the current pathStroke()- Stroke the current path
Properties:
fillColor- Fill color (Unity Color)strokeColor- Stroke color (Unity Color)lineWidth- Stroke width in pixelslineCap- Line cap style (Butt, Round, Square)lineJoin- Line join style (Miter, Round, Bevel)
Use MarkDirtyRepaint() to trigger a repaint when drawing state changes:
function AnimatedCircle() {
const ref = useRef<VisualElement>(null)
const [radius, setRadius] = useState(50)
useEffect(() => {
// Trigger repaint when radius changes
ref.current?.MarkDirtyRepaint()
}, [radius])
return (
<View
ref={ref}
style={{ width: 200, height: 200 }}
onGenerateVisualContent={(mgc) => {
const p = mgc.painter2D
p.fillColor = new CS.UnityEngine.Color(0, 0.5, 1, 1)
p.BeginPath()
p.Arc(
new CS.UnityEngine.Vector2(100, 100),
radius,
CS.UnityEngine.UIElements.Angle.Degrees(0),
CS.UnityEngine.UIElements.Angle.Degrees(360),
CS.UnityEngine.UIElements.ArcDirection.Clockwise
)
p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
}}
/>
)
}| Feature | Unity Painter2D | HTML5 Canvas |
|---|---|---|
| Transforms | Manual point calculation | Built-in translate/rotate/scale |
| Gradients | Limited (strokeGradient) | Full linear/radial/conic |
| State Stack | Not built-in | save()/restore() |
| Text | Via MeshGenerationContext.DrawText() | fillText/strokeText |
| Shadows | Not available | shadowBlur, shadowColor |
| Clipping | Via nested VisualElements | clip() path-based |
The following types are re-exported from unity-types:
type Vector2 = CS.UnityEngine.Vector2
type Color = CS.UnityEngine.Color
type Angle = CS.UnityEngine.UIElements.Angle
type ArcDirection = CS.UnityEngine.UIElements.ArcDirection
type Painter2D = CS.UnityEngine.UIElements.Painter2D
type MeshGenerationContext = CS.UnityEngine.UIElements.MeshGenerationContext
type GenerateVisualContentCallback = (context: MeshGenerationContext) => voidConverts C# collections (List<T>, arrays) to JavaScript arrays. C# collections exposed through the OneJS proxy have .Count/.Length and indexers but lack .map(), .filter(), and other array methods.
import { toArray } from "onejs-react"
// Convert a C# List for use in JSX
{toArray<Item>(inventory.Items).map(item => <ItemView key={item.Id} item={item} />)}
// Convert a C# array
const resolutions = toArray<Resolution>(Screen.resolutions)
// Safe with null — returns []
const npcs = toArray(currentPlace?.NPCs)Supports objects with .Count (List, IList) or .Length (C# arrays). Returns [] for null/undefined.
react-reconciler@0.31.x(React 19 compatible)vitest(dev) - Test runner- Peer:
react@18.x || 19.x