Skip to Content
We're building a full JS ecosystem for Unity, with OneJS at the core (open source release soon 🚀). Also cooking up a bunch of pre-made game UIs, plus a big site revamp in progress.

On this page we’ll go over common Preact and JSX usage in OneJS.

Intro

Preact is available to use in OneJS via the npm package onejs-preact. It’s a slightly modified version of Preact to work with Unity’s UI Toolkit and OneJS typings. We also included an alias path in the default tsconfig.json so that you can just use "preact" in your imports instead of "onejs-preact".

TextNodes

There’s no display: inline in UI Toolkit. Everything is flex. For this reason and how TextElement is implemented, it’s best to avoid TextNode segmentation in JSX. So instead of this:

<div>Character Name: {characterData.Name}</div>

You should do this:

<div>{`Character Name: ${characterData.Name}`}</div>

Hooks

All the built-in hooks like useState(), useEffect(), useMemo(), useRef(), etc. will work as expected.

hooks.tsx
import { h, render } from 'preact' import { useState, useEffect, useMemo, useRef } from 'preact/hooks' function App() { const [count, setCount] = useState(0) const renderCount = useRef(0) useEffect(() => { renderCount.current += 1 }) const double = useMemo(() => count * 2, [count]) return <div class="w-full h-full justify-center items-center"> <div>{`Count: ${count}`}</div> <div>{`Double: ${double}`}</div> <div>{`Component rendered: ${renderCount.current} times`}</div> <button onClick={() => setCount(count + 1)}>Increment</button> </div> } render(<App />, document.body)

onejs-preact also includes an useEventfulState() hook that allows you to easily subscribe to C# fields/events. Please refer to page UI Workflow or Ult Meter for more details.

Context

Context also works as expected. You can use createContext() and useContext() to create and consume context.

context.tsx
import { h, render } from 'preact' import { createContext } from 'preact' import { useContext, useState } from 'preact/hooks' interface CountContextType { count: number setCount: (count: number) => void } const CountContext = createContext({} as CountContextType) function App() { const [count, setCount] = useState(0) return <CountContext.Provider value={{ count, setCount }}> <div class="w-full h-full justify-center items-center"> <Display /> <Button /> </div> </CountContext.Provider> } function Display() { const { count } = useContext(CountContext) return <div>{`Count from context: ${count}`}</div> } function Button() { const { count, setCount } = useContext(CountContext) return <button onClick={() => setCount(count + 1)}>Increment</button> } render(<App />, document.body);

Signals

Preact Signals work fine as well.

signals.tsx
import { h, render } from 'preact' import { signal, effect, computed } from 'preact/signals' const count = signal(0) const double = computed(() => count.value * 2) effect(() => { console.log(`Count changed to: ${count.value}`) }) function App() { return <div class="w-full h-full justify-center items-center"> <div>{`Count: ${count.value}`}</div> <div>{`Double: ${double.value}`}</div> <button onClick={() => count.value++}>Increment</button> </div> } render(<App />, document.body)

Compat

memo

Optimizes performance by skipping re-renders when props haven’t changed.

memo.tsx
import { h, render } from 'preact' import { memo } from 'preact/compat' import { useState } from 'preact/hooks' interface RowProps { index: number data: string[] } const Row = ({ index, data }: RowProps) => { console.log(`Rendering Row ${index}`); return <div>{data[index]}</div>; }; const MemoizedRow = memo(Row, (prevProps, nextProps) => { return prevProps.index === nextProps.index && prevProps.data === nextProps.data; }); function App() { const [data, setData] = useState(['A', 'B', 'C']); const [other, setOther] = useState(0); return ( <div> {/* Clicking the button should not output anything (no re-render) */} <button onClick={() => setOther(o => o + 1)}>Trigger Rerender</button> <MemoizedRow index={1} data={data} /> </div> ); } render(<App />, document.body);

createPortal

Renders a component into a different part of the DOM tree (e.g., modals, tooltips).

modal.tsx
import { render, h } from "preact" import { createPortal } from "preact/compat" const App = () => { return <div class="w-full h-full justify-center items-center"> <Modal> <div class="bg-white p-4"> <div class="text-lg font-bold">Hello World</div> <div class="text-sm">This is a modal dialog</div> </div> </Modal> </div> } function Modal({ children }: { children?: any }) { return createPortal( <div class="modal">{children}</div>, document.getElementById('modal-root') ); } var container = document.createElement("div") var modalHolder = document.createElement("div") container.setAttribute("name", "container") modalHolder.setAttribute("name", "modal-root") document.body!.appendChild(container) document.body!.appendChild(modalHolder) render(<App />, container)

forwardRef

Passes a ref through a component to access a child DOM node or component instance.

forwardref.tsx
import { h, render } from 'preact' import { forwardRef } from 'preact/compat' import { useRef } from 'preact/hooks' interface InputProps { value: string } const Input = forwardRef<Element, InputProps>(({ value }, ref) => { return <textfield ref={ref} value={value} /> }) function App() { const inputRef = useRef<Element>(null) const focusInput = () => { inputRef.current?.focus() } return <div> <Input ref={inputRef} value="Hello there!" /> <button onClick={focusInput}>Focus Input</button> </div> } render(<App />, document.body)