npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ringozz/react-godot

v4.7.2-614

Published

A React renderer for Godot Engine via @ringozz/godot

Readme

@ringozz/react-godot

Declarative, component-driven Godot 4 scene graph management in React. Built for high-performance applications using @ringozz/godot.

Overview

@ringozz/react-godot brings a React Three Fiber (R3F)-like developer experience to Godot. It provides a custom React reconciler that renders React components directly into Godot's live engine scene tree (SceneTree).

If you are familiar with React Three Fiber, the core paradigms will feel instantly recognizable:

  • Declarative Nodes: Godot 3D/2D nodes (e.g., <RigidBody3D>, <Camera3D>, <DirectionalLight3D>) map directly to JSX components.
  • Resource Attachment: Use the attach prop to assign nested Godot resources (materials, shapes, meshes, environments) directly to parent properties.
  • Reactive Engine Loop: Use hooks like useSignal to hook into Godot signals (processFrame, windowInput) without triggering React state re-renders.

Installation & Setup

1. Dependencies

npm install @ringozz/react-godot react

2. TypeScript Configuration (tsconfig.json)

To enable custom JSX element resolution for Godot nodes, configure jsxImportSource and module settings in tsconfig.json:

{
  "compilerOptions": {
    "module": "esnext",
    "target": "es2025",
    "jsx": "react-jsx",
    "jsxImportSource": "@ringozz/react-godot",
    "rewriteRelativeImportExtensions": true,
    "verbatimModuleSyntax": true
  }
}

Application Entry & Portal Rendering

1. Mounting with createRoot(parent: Node)

createRoot is the primary entry point for mounting a React component tree into a Godot target Node (typically SceneTree.root).

import { Engine } from '@ringozz/godot/Engine';
import { SceneTree } from '@ringozz/godot/SceneTree';
import { createRoot } from '@ringozz/react-godot';
import { App } from './App';

const tree = Engine.getMainLoop() as SceneTree;
const { render } = createRoot(tree.root);

render(<App />);
  • Returns { render, unmount }.
  • Enables React StrictMode automatically in development (NODE_ENV === 'development').

2. Render Portals with createPortal(children, container)

createPortal allows rendering a React sub-tree into a different Godot Node in the hierarchy—such as a CanvasLayer for 2D UI overlays, a SubViewport, or an isolated container node.

import { createPortal } from '@ringozz/react-godot';
import { Label } from '@ringozz/godot/Label';

function Overlay({ overlayNode }: { overlayNode: Node }) {
  return createPortal(
    <Label text="HUD Overlay" position={[20, 20]} />,
    overlayNode
  );
}
  • Container Constraint: The target container must be a Godot Node (it cannot target Resource instances).
  • Cleanup: Unmounting the portal safely disposes of and frees its child Godot nodes.

Core Concepts & Patterns

1. Existing Objects with object Prop

If you already have a pre-existing Godot Node, Resource, or PackedScene instance (e.g. instantiated from C++ or loaded from disk), pass it via the object prop to adopt it into the React component tree instead of letting the reconciler instantiate a new object:

<Node object={myExistingGodotNode}>
  {/* Children attached to existing node */}
</Node>
  • object is immutable on a mounted instance. It selects the host instance once, at creation. A re-render that changes object on an existing element throws (Cannot change the \object` of a mounted instance) because React cannot swap a fiber's Godot node — remount with a new key` instead.
  • A PackedScene object is instantiated once per host mount — each element gets a fresh node tree; the result is a plain Node (not .reference()'d) freed on unmount. The same PackedScene can feed multiple elements; give same-type siblings stable keys.
  • Dynamic same-type sibling lists need stable keys. Godot host nodes are identity, not content: React reconciles keyless siblings by position, so deleting the first of two same-type siblings reuses its Godot node for the survivor. A plain state re-render does this too, and Bun's web dev server makes it especially likely — its React Fast Refresh re-renders the edited component in place (no app re-run), reusing the same fibers and Godot nodes, so a deleted sibling's node can survive on its neighbor and the neighbor's node is freed. Add key="..." to each dynamically toggled same-type sibling to keep nodes pinned to the right elements.

1b. Attaching a Script with script

Use the script prop to attach a Script resource (e.g. an imported GDScript .gd module) to a node on creation. The script is attached before the node enters the tree, so Godot fires _ready and auto-enables the script's _process/_input — no manual set_process(true)/set_process_input(true) calls needed:

import controller from './camera_controller.gd';

<Camera3D script={use(controller)} current={true} fov={55} />
  • Attach before tree entry means _ready/_process/_input work. Attaching to an already-in-tree node (e.g. in a useEffect) never fires _ready and leaves _process/_input disabled — prefer script over an effect for scripted nodes.
  • script is immutable on a mounted instance. A re-render that changes script on an existing element throws — remount with a new key to swap the compiled script.

2. Resource Nesting with attach

In Godot, nodes often hold references to Resource objects (such as shapes, meshes, materials, or environments). Use the attach prop to automatically assign a child resource to a specific property on its parent:

/* Attaching a shape to CollisionShape3D */
<CollisionShape3D>
  <BoxShape3D attach="shape" size={[1, 1, 1]} />
</CollisionShape3D>

/* Attaching a material override to MeshInstance3D */
<MeshInstance3D>
  <BoxMesh attach="mesh" />
  <StandardMaterial3D attach="materialOverride" albedoColor={[1, 0, 0]} />
</MeshInstance3D>

/* Attaching physics material to RigidBody3D */
<RigidBody3D>
  <PhysicsMaterial attach="physicsMaterialOverride" bounce={0.8} />
</RigidBody3D>

/* Attaching environment to WorldEnvironment */
<WorldEnvironment>
  <Environment attach="environment" backgroundColor={[0.1, 0.1, 0.2]} />
</WorldEnvironment>

Note for physics bodies: attach="shape" goes on the shape resource element (e.g. <BoxShape3D attach="shape" />), never on the CollisionShape3D node itself — Node.add_child requires a Node, and shapes are Resources, so an attach on CollisionShape3D adds the shape as a child node and fails. Likewise, mesh children of a physics body must be wrapped in a MeshInstance3D (a RigidBody3D/StaticBody3D has no mesh/materialOverride props to attach to).

3. Properties & Helper Types

Props map directly to Godot node setters and properties. Helper arrays are automatically cast to Godot types:

  • Vectors / value types: value types are named tuples — flat (Vector3, Color, Rect2, …) are scalar-labeled tuples (Vector3 = [x, y, z], Color = [r, g, b, a?]); nested (AABB/Transform2D/Basis/Transform3D/Projection) are row-labeled tuples of the flat element alias (Transform3D = [x, y, z, origin], each row a Vector3), so position={[0, 1, 0]}, transform={[[1, 0, 0], [0, 1, 0], [0, 0, 1], [10, 20, 30]]}. Getters return the same labeled shape (node.position is [x, y, z]); direct setters accept them too (node.position = [1, 2, 3]). Wrong-arity arrays fail typecheck (position={[1, 2]} errors) — except Color's trailing a, which is optional ([r, g, b, a?], alpha defaults 1; it's the only value type with a partial scalar ctor). The math API lives as named functions per type module — import * as v3 from '@ringozz/godot/Vector3', then v3.normalized(v), v3.dot(a, b); construction uses fromXxx factories (basis.fromAxisAngle(axis, angle), transform3d.fromBasisOrigin(basis, origin)).
  • Colors: albedoColor={[1, 0.2, 0.2]} (a Color is just [r, g, b, a?]).
  • Packed arrays: Packed*Array-typed props (e.g. Gradient.colors/offsets) accept plain JS arrays — elements convert with the array's element type, so colors={[[0.5, 0, 0, 1], [1, 1, 1, 1]]} works; new PackedVector2Array([[1, 2], [3, 4]]) works too. The same element-aware conversion applies to direct setters and method args: gradient.colors = [[1, 0, 0, 1], [0, 1, 0, 1]], addPoint(0.5, [1, 0, 0, 1]), and PhysicsRayQueryParameters3D.create([1, 2, 3], [4, 5, 6]) all accept tuples.
  • Array-typed props (GodotArray, e.g. CodeEdit.lineLengthGuidelines, Font.fallbacks) accept plain JS arrays directly — they become a Godot Array.
  • Sub-properties / Dash props: Set individual vector/color components directly using dash syntax:
    <BoxMesh attach="mesh" size-x={0.5} size-y={1.0} size-z={0.5} />

Text content: text nodes are unsupported, but string/number children — and arrays of all-string/all-number children — are flattened into the node's text prop, so <Label>Hello</Label> works. Mixed children (containing elements) throw; use an explicit text prop instead, and never pass string children to a node without a text prop.

4. Engine Signals & Event Subscription (useSignal)

Subscribe to Godot engine signals cleanly with useSignal.

import { Engine } from '@ringozz/godot';
import { SceneTree } from '@ringozz/godot/SceneTree';
import { useSignal } from '@ringozz/react-godot';

const tree = Engine.getMainLoop() as SceneTree;
const root = tree.root;

// Subscribe to frame updates (process loop)
useSignal(tree.processFrame, () => {
  // Update animations, camera matrix, physics targets, etc.
});

// Subscribe to window input events
useSignal(root.windowInput, (event) => {
  if (event instanceof InputEventMouseMotion) {
    // Handle mouse motion
  }
});

Signal JSX props (pressed, toggled, valueChanged, textSubmitted, ...) are typed as the signal's callback — pass a plain function, no cast needed:

<Button text="Play" pressed={() => start()} />
<CheckButton text="Debug" toggled={(on) => setDebug(on)} />
<HSlider value={round} valueChanged={(v) => setRound(v)} />

Signal handlers are connected before value props are applied within a commit (Instance.assign), so the handler a render installs is already current when a value setter fires its signal (e.g. Range.value emits value_changed). Note that programmatic value sets still emit (Godot semantics) — if you feed a slider's value back from your own state, guard the handler (e.g. compare against the live store) or the emission loops back into your reducer.

ComponentProps only exposes writable members: read-only props (e.g. Node.multiplayer) and methods are not settable props.

Full Example

Below is a complete interactive 3D physics scene with custom camera controls and physics bodies:

import { Engine, Key, MouseButton } from '@ringozz/godot';
import type { Color } from '@ringozz/godot';
import { BoxMesh } from '@ringozz/godot/BoxMesh';
import { BoxShape3D } from '@ringozz/godot/BoxShape3D';
import { Camera3D } from '@ringozz/godot/Camera3D';
import { CollisionShape3D } from '@ringozz/godot/CollisionShape3D';
import { DirectionalLight3D } from '@ringozz/godot/DirectionalLight3D';
import { BGMode, Environment, ToneMapper } from '@ringozz/godot/Environment';
import { InputEventMouseButton } from '@ringozz/godot/InputEventMouseButton';
import { InputEventMouseMotion } from '@ringozz/godot/InputEventMouseMotion';
import { MeshInstance3D } from '@ringozz/godot/MeshInstance3D';
import { PhysicsMaterial } from '@ringozz/godot/PhysicsMaterial';
import { RigidBody3D } from '@ringozz/godot/RigidBody3D';
import { SceneTree } from '@ringozz/godot/SceneTree';
import { SphereMesh } from '@ringozz/godot/SphereMesh';
import { SphereShape3D } from '@ringozz/godot/SphereShape3D';
import { StandardMaterial3D } from '@ringozz/godot/StandardMaterial3D';
import { StaticBody3D } from '@ringozz/godot/StaticBody3D';
import { WorldEnvironment } from '@ringozz/godot/WorldEnvironment';
import { useSignal, type ComponentProps } from '@ringozz/react-godot';
import { useRef, type ReactElement } from 'react';

const tree = Engine.getMainLoop() as SceneTree;
const root = tree.root;

function PhysBody({ color, shape, children, ...rest }: {
  color: Color;
  shape: ReactElement;
} & ComponentProps<typeof RigidBody3D>) {
  return (
    <RigidBody3D {...rest}>
      <MeshInstance3D>
        {children}
        <StandardMaterial3D attach="materialOverride" albedoColor={color} />
      </MeshInstance3D>
      <CollisionShape3D>{shape}</CollisionShape3D>
    </RigidBody3D>
  );
}

export function App() {
  const cameraRef = useRef<Camera3D>(null);
  const isDragging = useRef(false);
  const yaw = useRef(0.7);
  const pitch = useRef(0.3);
  const zoom = useRef(7);

  useSignal(root.windowInput, (event) => {
    if (event instanceof InputEventMouseMotion && isDragging.current) {
      yaw.current -= event.relative[0] * 0.005;
      pitch.current = Math.max(-1.4, Math.min(1.4, pitch.current + event.relative[1] * 0.005));
    } else if (event instanceof InputEventMouseButton) {
      if (event.buttonIndex === MouseButton.MOUSE_BUTTON_LEFT) {
        isDragging.current = event.pressed;
      }
    }
    event.free();
  });

  useSignal(tree.processFrame, () => {
    const camera = cameraRef.current;
    if (!camera) return;

    const cx = zoom.current * Math.cos(pitch.current) * Math.sin(yaw.current);
    const cy = zoom.current * Math.sin(pitch.current) + 1;
    const cz = zoom.current * Math.cos(pitch.current) * Math.cos(yaw.current);
    camera.position = [cx, cy, cz];
    camera.lookAt([0, 1, 0], [0, 1, 0]);
  });

  return (
    <>
      <WorldEnvironment>
        <Environment attach="environment" tonemapMode={ToneMapper.TONE_MAPPER_AGX} backgroundMode={BGMode.BG_COLOR} backgroundColor={[0.05, 0.1, 0.25]} />
      </WorldEnvironment>
      <DirectionalLight3D name="Sun" rotation={[-0.8, 0.5, 0]} />
      <Camera3D name="Camera" ref={cameraRef} />

      {/* Physics Sphere */}
      <PhysBody name="BouncySphere" position={[0, 4, 0]} color={[0.2, 1, 0.2]}
        shape={<SphereShape3D attach="shape" radius={0.5} />}>
        <SphereMesh attach="mesh" radius={0.5} height={1} />
        <PhysicsMaterial attach="physicsMaterialOverride" bounce={0.8} />
      </PhysBody>

      {/* Ground Plane */}
      <StaticBody3D name="Ground">
        <CollisionShape3D>
          <BoxShape3D attach="shape" size={[10, 0.1, 10]} />
        </CollisionShape3D>
        <MeshInstance3D>
          <BoxMesh attach="mesh" size-x={10} size-y={0.1} size-z={10} />
          <StandardMaterial3D attach="materialOverride" albedoColor={[0.2, 0.2, 0.25]} />
        </MeshInstance3D>
      </StaticBody3D>
    </>
  );
}

API Summary

| Export | Type | Description | | :--- | :--- | :--- | | createRoot(parent) | Function | Mounts a React component tree into a target Godot Node (e.g. tree.root). Returns { render, unmount }. | | createPortal(children, container) | Function | Renders children into a target Godot Node (e.g. CanvasLayer or SubViewport). | | useSignal(signal, callback) | Hook | Connects a callback to a Godot object signal (e.g. tree.processFrame, node.treeEntered). Disconnects on unmount. | | useMutableCallback(fn) | Hook | Returns a RefObject whose .current always points to the latest callback implementation. | | useTween<NodeT>(create, deps) | Hook | Reactive tween-as-prop-value. Returns [spring, tweenRef] — spread spring onto a node; tweenRef.current is the live native Tween. The optional node type (useTween<Node3D>) types to/from array goals as that node's value-type tuples. | | ComponentProps<T> | Type | Utility type to infer valid React JSX props for any Godot node class T. |

Animating props with useTween

useTween animates a node's properties declaratively. You describe the target values once per to key; spreading the returned spring onto a node binds the tween to it. Re-rendering with the same deps keeps the same tween (nothing restarts); changing deps creates a new Tween and animates from the current values.

const [spring, tweenRef] = useTween<Node3D>(() => ({
  from: { position: [0, 0, 0] },
  to:   { position: [0, 2, 0], globalRotation: [0, 90, 0] },
  config: { duration: 0.6, transition: TransitionType.TRANS_QUAD, ease: EaseType.EASE_IN_OUT },
  delay: 0, loops: 1, immediate: false,
  onFinished: () => {},
}), [x, z]);

return <Node3D {...spring} />;
  • spring holds one function per to key (spread them all onto the node; each contributes its own prop's tweener to the shared Tween). Array values are converted to the property's value type.
  • Node-typed goals: call useTween<Node3D>(...) so to/from are typed against that node's props — position: [0, 2, 0] and nested basis: [[…]] infer as Vector3/Basis, with no as Vector3 casts. Only scalar-number and value-type props are tweenable (signals, Node refs, bools, strings, containers are rejected); the returned spring is that whole prop set (all optional), so {...spring} stays JSX-spreadable. A bare useTween(...) keeps array goals loose (number[]).
  • tweenRef.current is the live native Godot Tween, created when the node mounts (null before that): tweenRef.current?.kill(), ?.pause(), ?.play(), ?.setSpeedScale(n), ?.isRunning(), ?.finished
  • config maps to the Godot Tween API: duration (seconds), transition/ease (TransitionType/EaseType — the tween's defaults), easing (a [0,1]→[0,1] function → setCustomInterpolator), speedScale.
  • onStart fires when the tween is created/bound (on mount or when deps change), before it starts stepping.
  • onFinished fires on natural completion only (Godot's finished signal); kill()/unmount don't fire it, so no stale callbacks.
  • Unwrapping: replacing a springed prop with a plain value (e.g. dropping {...spring} for position={[9, 0, 0]}) does not stop the running tween — it keeps animating to its target and the tween's value wins over the plain value. Stop it with tweenRef.current?.kill().