Mount a scene in a React web app.
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]);}
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>);}
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 />;}