Contributing

Running the repo, the package layout, linting, tests and changesets.

The library is alpha and moving; issues and PRs are welcome. Everything below assumes a clone of pmndrs/react-three-jolt.

Setup

Node 22 and Yarn 4 (a Yarn 4 monorepo). With Corepack enabled you get the right Yarn automatically:

corepack enable
yarn install
yarn build

Layout

path
packages/react-three-jolt@react-three/jolt — components, hooks, systems
packages/react-three-jolt/src/addons@react-three/jolt/addons — input commands, helper components
packages/react-three-jolt/src/controllers@react-three/jolt/controllers — character, camera rig, vehicles
apps/examplesthe Vite demo app
docs/this site (MDX)

Inside the core package: components/ (the React layer), systems/ (the physics layer — physics-system, body-system, body-state, constraint-system, shape-system, queries/), hooks/, utils/, and raw.ts, which holds the one global reference to the Jolt WASM module.

The split matters: components own React lifecycle and props, systems own Jolt. A component should not talk to Raw.module.

Running the examples

cd apps/examples
yarn dev

For HMR against your library changes, run a watch build in a second terminal:

cd packages/react-three-jolt
yarn build -w

Docs

This site is MDX in docs/, built by pmndrs/docs.

yarn docs        # build and serve locally
yarn docs:build  # static build into docs/out

Front matter is title, description and nav — one ordering number shared across every page. DEVELOPMENT.md has the full conventions and the GitHub Pages setup.

Two rules for doc pages:

  • Internal links are /section/page#anchor, where the page is docs/section/page.mdx and the anchor is the slug of a heading on it (lowercase, punctuation dropped, spaces to hyphens). A broken anchor renders as a link to the top of the page rather than an error, so check them.
  • Every ts / tsx code sample is expected to typecheck against the built dist. The cheapest way to verify a sample is to drop it into a scratch file under apps/examples/src and run tsc -p apps/examples. A sample that can't (a Next.js snippet, a partial JSX fragment) should be obviously partial.

Lint, format, test

yarn lint    # biome check .
yarn format  # biome format --write .
yarn test    # vitest across the workspaces

Biome replaced ESLint + Prettier: 4-space indent, 100-column lines, single quotes, no trailing commas. yarn lint runs in CI and must exit 0.

Caution

Do not run biome check --write (and never --unsafe) across files you didn't touch. Two of its "safe" fixes break this codebase's build: rewriting @ts-ignore to @ts-expect-error (many of those suppress errors that only appear on some type-narrowing paths) and splitting a default React import into import type. Format only your own files: yarn biome format --write <paths>. LINTING.md lists the rules currently downgraded to warn and why.

Merging

Caution

Git merges duplicate methods silently. Two branches that each add a destroy(), a listener array or a dispose() to the same class merge cleanly into a class with the method twice — no conflict, and in JavaScript the last definition wins, so the first branch's teardown disappears without a single error. Before you add a teardown method, a listener list or an on* helper to an existing class, grep for one first; after a merge, grep the file again.

The same applies to the listener convention: emitter.on(type, fn) returns an unsubscribe and nothing is ever removed by function identity. If you find yourself writing a removeXListener(fn), there is almost certainly one already — and it is almost certainly deprecated. PhysicsSystem.events.listenerCount(type) is the supported test hook for asserting that a subscription was actually dropped.

Tests

Vitest 5 with happy-dom, and the real Jolt WASM module running in Node — the tests are not mocked. Component tests use @react-three/test-renderer.

If you touch anything that allocates Jolt objects, write an allocation-counting test: test/jolt-alloc.ts exposes installAllocTracker(Raw), which wraps the module's constructors and destroy so a test can assert the live-object count is flat across many iterations. It catches leaks (count grows) and double frees (count drifts negative) — neither of which throws on its own. Note that the tracker swaps Raw.module's identity, which rebuilds the joltScratch singletons once, so warm them before you start counting.

Commits and changesets

Conventional commits (feat:, fix:, chore:, docs:, refactor:).

Every change that affects a published package needs a changeset:

yarn change

Pick the packages, pick patch for fixes and minor for features, and describe what changed for a user — the existing changesets in .changeset/ are long on purpose and are the repository's real changelog. A GitHub Action opens the version-bump PR from them.

Docs-only and example-only changes don't need one.

CI

Every pull request runs .github/workflows/ci.yml on Node 22: yarn install --immutable, yarn lint, yarn build, yarn test. Run all four locally before opening a PR. Dependabot handles grouped weekly dependency bumps, excluding majors of three, react and @react-three/* — those are deliberate, hand-verified upgrades.

Working on the physics layer

Three things to read before your first PR into systems/:

  1. Memory & lifecycle — ownership, by-value static temporaries, and the helpers that keep you out of trouble.
  2. packages/react-three-jolt/docs/Memory.md — the original engine-level notes.
  3. The Jolt docs — the JS API mirrors the C++ one, and JoltJS.idl is the authoritative list of what is exposed to JavaScript.

The IDL is a declaration, not a promise about what Jolt actually calls. Measured against jolt-physics 1.1.0, a CharacterVirtual stepping against bodies invokes six of CharacterContactListenerJS's eleven declared callbacks (OnAdjustBodyVelocity, OnContactValidate, OnContactAdded, OnContactPersisted, OnContactRemoved, OnContactSolve); the five character-vs-character ones only fire once a CharacterVsCharacterCollision is installed. Emscripten's hasOwnProperty check is lazy — one per call site — so the unused ones need no stubs. If you are about to add a no-op callback "because the IDL says so", write a test that fails when Jolt starts calling it instead (packages/react-three-jolt/test/controllers/character-contact-listener.test.ts is the pattern).