Use the /collab command on Discord to gain access to the OneJS private repo. The repo offers early access to the latest features and fixes that may not yet be available on the Asset Store. An early preview of OneJS V2 is available on branch onejs-v2. It brings major performance improvements, zero-allocation interop, and a new esbuild workflow.
VERSION
Doc Menu

C#-JS Workflow

OneJS uses Jint to glue C# and TS together. You can access any .Net Assemblies/Namespace/Classes from Javascript:

var { GameObject, MeshRenderer, Color } = importNamespace("UnityEngine")
var sphere = GameObject.CreatePrimitive(0)
sphere.GetComponent(MeshRenderer).material.color = Color.red

With Type Definitions, the same code can be written in Typescript as:

import { GameObject, PrimitiveType, MeshRenderer, Color } from "UnityEngine"

const sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere)
sphere.GetComponent(MeshRenderer).material.color = Color.red

To pass .Net objects to TS, you can use the ScriptEngine/INTEROP/Objects list:

ProgressManager can now be accessible from TS via require("pman")

The INTEROP/Objects list accepts any UnityEngine.Object. This includes things like GameObject, MonoBehaviour, ScriptableObject, RenderTexture, Sprite, etc.

UI Dataflow

In general, your UI code should depend on your core game logic. But your core game logic should not even be aware of the existence of your UI code. In other words, your TS code will be calling stuff from your C# code, but never the other way around. This one-directional dependency makes everything easy to maintain.

The best way to implement this is via C# events (or similar pub/sub mechanisms). Whenever your UI needs something, you can have your core game logic fire an event. And in your TS code, you can subscribe to C# events by appending "add_" and "remove_" to the event name.

Here's a quick example:

// C#
public class TreasureChestSpawner : MonoBehaviour {  // Map this object to be 'chest-spawner' in ScriptEngine's INTEROP/Objects list
    public event Action OnChestSpawned; // Fired when a chest is spawned in the scene
    
    ...
}
// TS
var spawner = require("chest-spawner") as MyGame.ChestSpawner
spawner.add_OnChestSpawned(onChestSpawned)

function onChestSpawned() {
    log("yay!")
}

// Event handler can be removed via `spawner.remove_OnChestSpawned(onChestSpawned)`
// Example TS Definition
declare namespace MyGame {
    export interface ChestSpawner {
        add_OnChestSpawned(handler: Function): void
        remove_OnChestSpawned(handler: Function): void
    }
}

You can see this workflow in more detail from the Fortnite UI sample.

Reducing Boilerplates

C# events need to be properly cleaned up from the JS/Preact side. Compound that with the "add_" and "remove_" event syntax, you usually end up with a bit of verbose boilerplate. This is where you can make use of OneJS's useEventfulState() function to reduce the following boilerplate:

const App = () => {
    var charman = require("charman")

    const [health, setHealth] = useState(charman.Health)
    const [maxHealth, setMaxHealth] = useState(charman.MaxHealth)

    useEffect(() => {
        charman.add_OnHealthChanged(onHealthChanged)
        charman.add_OnMaxHealthChanged(onMaxHealthChanged)

        onEngineReload(() => {  // Cleaning up for Live Reload
            charman.remove_OnHealthChanged(onHealthChanged)
            charman.remove_OnMaxHealthChanged(onMaxHealthChanged)
        })

        return () => {  // Cleaning up for side effect
            charman.remove_OnHealthChanged(onHealthChanged)
            charman.remove_OnMaxHealthChanged(onMaxHealthChanged)
        }
    }, [])

    function onHealthChanged(v: number): void {
        setHealth(v)
    }

    function onMaxHealthChanged(v: number): void {
        setMaxHealth(v)
    }

    return <div>...</div>
}

To just:

const App = () => {
    var charman = require("charman")

    const [health, setHealth] = useEventfulState(charman, "Health")
    const [maxHealth, setMaxHealth] = useEventfulState(charman, "MaxHealth")

    return <div>...</div>
}

useEventfulState() will take care of the event subscription and cleanup for you automatically. This is demonstrated in the Overwatch UI sample.

NOTE: useEventfulState(obj, "Health") assumes the C# obj has a property named "Health" and an event named "OnHealthChanged".

C# Source Generator

You may also use the EventfulProperty attribute to further reduce boilerplates on the C# side and turn this:

public class Character : MonoBehaviour {
    public float Health {
        get { return _health; }
        set {
            _health = value;
            OnHealthChanged?.Invoke(_health);
        }
    }
    
    public event Action<float> OnHealthChanged;
    
    public float MaxHealth {
        get { return _maxHealth; }
        set {
            _maxHealth = value;
            OnMaxHealthChanged?.Invoke(_maxHealth);
        }
    }
    
    public event Action<float> OnMaxHealthChanged;

    float _health = 200f;
    float _maxHealth = 200f;
}

into just this:

public partial class Character : MonoBehaviour {
    [EventfulProperty] float _health = 200f;
    [EventfulProperty] float _maxHealth = 200f;
}

Note the partial keyword being used on the class declaration. The corresponding getters, setters, and events will be auto-created by Source Generators.