Web APIs#
OneJS provides browser-compatible APIs so JavaScript libraries and patterns work as expected. These are implemented using Unity's underlying systems.
localStorage#
Persist data using Unity's PlayerPrefs:
// Store data
localStorage.setItem("username", "player1")
localStorage.setItem("settings", JSON.stringify({ volume: 0.8 }))
// Retrieve data
const username = localStorage.getItem("username") // "player1"
const settings = JSON.parse(localStorage.getItem("settings"))
// Remove data
localStorage.removeItem("username")
// Clear all data
localStorage.clear()Limitations#
Unlike browser localStorage:
localStorage.key(index)always returnsnull(PlayerPrefs doesn't support enumeration)localStorage.lengthalways returns0- Data persists permanently (no expiration)
Example: Saving Game State#
function usePersistentState(key, defaultValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : defaultValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue]
}
function Settings() {
const [volume, setVolume] = usePersistentState("volume", 1.0)
return (
<Slider
value={volume}
lowValue={0}
highValue={1}
onChange={(e) => setVolume(e.value)}
/>
)
}sessionStorage#
sessionStorage is an alias to localStorage in OneJS. Unity doesn't have browser-style sessions, so data persists across app restarts.
If you need session-scoped data, clear on app start:
// At app initialization
const SESSION_KEY = "__session_id"
const currentSession = Date.now().toString()
const lastSession = localStorage.getItem(SESSION_KEY)
if (lastSession !== currentSession) {
// Clear session data from previous run
localStorage.removeItem("temp_data")
localStorage.setItem(SESSION_KEY, currentSession)
}URL & URLSearchParams#
Parse and manipulate URLs:
// Parse a URL
const url = new URL("https://api.example.com:8080/users?page=1&limit=10#section")
console.log(url.hostname) // "api.example.com"
console.log(url.port) // "8080"
console.log(url.pathname) // "/users"
console.log(url.search) // "?page=1&limit=10"
console.log(url.hash) // "#section"
console.log(url.origin) // "https://api.example.com:8080"
// Modify URL
url.pathname = "/posts"
url.searchParams.set("page", "2")
console.log(url.href) // "https://api.example.com:8080/posts?page=2&limit=10#section"URLSearchParams#
Work with query strings:
// Create from string
const params = new URLSearchParams("foo=1&bar=2")
// Get values
params.get("foo") // "1"
params.getAll("foo") // ["1"]
params.has("bar") // true
// Modify
params.set("foo", "new")
params.append("baz", "3")
params.delete("bar")
// Iterate
params.forEach((value, key) => {
console.log(`${key}: ${value}`)
})
// Convert to string
params.toString() // "foo=new&baz=3"API Request Helper#
function buildApiUrl(endpoint, params = {}) {
const url = new URL(endpoint, "https://api.example.com")
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, value)
})
return url.href
}
// Usage
const url = buildApiUrl("/users", { page: 1, limit: 20 })
// "https://api.example.com/users?page=1&limit=20"Base64 Encoding#
Encode and decode Base64:
// Encode string to Base64
const encoded = btoa("Hello, World!")
console.log(encoded) // "SGVsbG8sIFdvcmxkIQ=="
// Decode Base64 to string
const decoded = atob("SGVsbG8sIFdvcmxkIQ==")
console.log(decoded) // "Hello, World!"Use Cases#
// Encode JSON for URL-safe transmission
const data = { id: 123, name: "test" }
const encoded = btoa(JSON.stringify(data))
// Basic auth header
const credentials = btoa(`${username}:${password}`)
const headers = { Authorization: `Basic ${credentials}` }Timers#
Standard JavaScript timing functions:
// One-time delay
const timeoutId = setTimeout(() => {
console.log("Executed after 1 second")
}, 1000)
clearTimeout(timeoutId) // Cancel
// Repeated interval
const intervalId = setInterval(() => {
console.log("Every 500ms")
}, 500)
clearInterval(intervalId) // Stop
// Immediate execution (microtask)
const immediateId = setImmediate(() => {
console.log("Runs immediately after current code")
})
clearImmediate(immediateId) // Cancel
// Animation frame (synced with Unity Update)
function animate() {
// Update animation
requestAnimationFrame(animate)
}
const rafId = requestAnimationFrame(animate)
cancelAnimationFrame(rafId) // StopReact Timer Patterns#
// Interval hook
function useInterval(callback, delay) {
const savedCallback = useRef(callback)
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
if (delay === null) return
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}, [delay])
}
// Usage
function Clock() {
const [time, setTime] = useState(new Date())
useInterval(() => {
setTime(new Date())
}, 1000)
return <Label text={time.toLocaleTimeString()} />
}performance.now()#
High-precision timestamp for performance measurement:
const start = performance.now()
// Do work
expensiveOperation()
const end = performance.now()
console.log(`Took ${end - start}ms`)Note: In WebGL, this is the native browser API. On other platforms, it returns the time since JavaScript started.
queueMicrotask#
Schedule code to run after current task but before rendering:
queueMicrotask(() => {
console.log("Runs after current synchronous code")
})
console.log("This logs first")Useful for batching updates:
let pendingUpdates = []
let scheduled = false
function scheduleUpdate(update) {
pendingUpdates.push(update)
if (!scheduled) {
scheduled = true
queueMicrotask(() => {
const updates = pendingUpdates
pendingUpdates = []
scheduled = false
// Process all batched updates
updates.forEach(u => u())
})
}
}StyleSheet APIs#
See Styling: StyleSheet API for runtime stylesheet management (loadStyleSheet, compileStyleSheet, removeStyleSheet, clearStyleSheets).
Browser Detection in Libraries#
On native platforms OneJS intentionally does not define window or document. Many npm libraries check those globals to detect a browser, and silently switch to a non-browser fallback when they are missing. The library still runs, but a feature you expected may be quietly disabled.
A concrete example: with supabase-js, sign-in succeeds but the session never survives an app restart. Its auth module only persists sessions to localStorage when both window and document exist, so under OneJS it silently falls back to in-memory storage. The fix is to pass an explicit storage adapter, which its code path uses unconditionally:
import { createClient } from "@supabase/supabase-js"
const supabase = createClient(url, anonKey, {
auth: {
storage: {
getItem: (key) => localStorage.getItem(key),
setItem: (key, value) => localStorage.setItem(key, value),
removeItem: (key) => localStorage.removeItem(key),
},
},
})When a library "works but forgets" or skips a documented feature under OneJS, check its browser detection paths first, then look for an option to inject the dependency explicitly (storage, fetch, WebSocket, and so on). Do not define fake window or document globals to satisfy the detection: other libraries will then assume a full browser and fail in worse ways.
Platform Differences#
| API | Native (QuickJS) | WebGL (Browser) |
|---|---|---|
localStorage | PlayerPrefs | Native localStorage |
fetch | UnityWebRequest | Native fetch |
WebSocket | C# ClientWebSocket | Native WebSocket |
URL | Polyfill | Native URL |
performance.now() | JS engine time | Native high-res time |
setTimeout | Unity Update loop | Native timers |
requestAnimationFrame | Unity Update loop | Native RAF |
The same code works on both platforms. OneJS uses native browser APIs in WebGL for better performance.