React browser quickstart

Mount a scene in a React web app.

Write the scene

Keep the scene in plain functions so React only mounts it. useWorld calls setupScene once with a fresh world after the engine loads, and builds a new one when its deps change.

scene.ts

React and React Native · shared engine API

scene.ts
import type { Camera, World } from "@nayan-ui/engine";
export const camera: Camera = { eye: [8, 7, 10], target: [0, 1, 0], fov: 0.9 };
export function setupScene(world: World) {
world.spawn({ mesh: "cube", position: [0, -0.5, 0], scale: [16, 1, 16], color: "#404D59", physics: "fixed" });
world.spawn({ mesh: "sphere", position: [0, 3, 0], color: "#33A6FF", physics: { type: "dynamic", bounce: 0.65 } });
}
/** Tapping the ball bounces it. */
export function tapScene(world: World, x: number, y: number) {
const hit = world.pick(x, y);
if (hit) world.impulse(hit.entity, [0, 5, 0]);
}

Mount it

useWorld returns null until the world is ready and passes start-up failures, such as the WebAssembly failing to load, to onError. GameView needs a container with a real height; paused stops updates and drawing but keeps the world.

Game.tsx

React · browser

Game.tsx
"use client";
import { useState } from "react";
import { GameView, useWorld } from "@nayan-ui/engine";
import { camera, setupScene, tapScene } from "./scene";
export default function Game() {
const [error, setError] = useState<string | null>(null);
const [paused, setPaused] = useState(false);
const world = useWorld(64, setupScene, [], (reason) => setError(reason.message));
if (error) return <p role="alert">{error}</p>;
if (!world) return <p>Loading…</p>;
return (
<main style={{ height: "100dvh", display: "flex", flexDirection: "column" }}>
<button onClick={() => setPaused((value) => !value)}>{paused ? "Resume" : "Pause"}</button>
<GameView source={world} camera={camera} paused={paused} style={{ flex: 1, minHeight: 0 }}
onTap={(x, y) => tapScene(world, x, y)} onError={(reason) => setError(reason.message)} />
</main>
);
}

Next.js and Vite

In the Next.js App Router, mark the screen "use client" and load it with dynamic(..., { ssr: false }) so worlds are only created in the browser. In Vite, mount it with createRoot(root).render(<Game />); React Native Web isn't needed.

page.tsx

React · browser

page.tsx
"use client";
import dynamic from "next/dynamic";
const Game = dynamic(() => import("./Game"), { ssr: false });
export default function GamePage() {
return <Game />;
}