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