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

foundry-vtt-react

v0.1.5

Published

Extensions of various FoundryVTT classes to support React applications.

Readme

Foundry VTT React

This package provides a development and build harness for creating React applications within Foundry. It also includes extensions of various Foundry VTT classes to support building React applications inside Foundry VTT. This is a bit experimental, but enables a modern JS workflow and if you know and enjoy React workflows with HMR/fast-refresh, this should have you flying in no time.

Disclaimers:

  • Only tested against Foundry v13
  • Not a Foundry module, but a build dependency to add to your module's build workflow and dev setup
  • Relies on Vitejs building.
  • TypeScript friendly!

Installation

npm install foundry-vtt-react
# or: pnpm add foundry-vtt-react

react and react-dom (v19) are peer dependencies — install them in your module if you haven't already:

npm install react react-dom

Supported Foundry classes

  • ReactApplicationV2 — a base ApplicationV2 that renders a React component instead of a Handlebars template.
  • ReactActorSheetV2 — an ActorSheetV2 that renders with React, letting your component react to document changes through the native sheet lifecycle.

Both are produced by the same ReactApplicationMixin, so they share the options and lifecycle described below.

Usage

There are two pieces to developing React applications in Foundry:

  1. Creating a React-powered application instance and passing in your React component.
  2. Configuring a Vite dev server to work with your local Foundry instance (see Development setup).

Creating a ReactApplicationV2 instance

Instantiate ReactApplicationV2 with your React component plus any initial props and window options:

import { ReactApplicationV2 } from "foundry-vtt-react";

// Basic component
function MyReactComponent(props) {
  return <div>Hello, {props.data}!</div>;
}

// Declare an instance and render it as a Foundry application
const app = new ReactApplicationV2({
  reactApp: MyReactComponent,
  initialProps: { data: "example" },
  window: { title: "My React App" },
  position: { width: 300, height: 200 },
});
app.render(true);

A React component rendered inside a Foundry application window

Constructor options

| Option | Type | Description | | -------------- | --------------------- | ---------------------------------------------------------------------------------------- | | reactApp | React.ComponentType | The component mounted into the application window. | | initialProps | object (optional) | Props passed to reactApp on mount. Also reachable via _prepareContext (see below). | | ...options | ApplicationV2 | Any standard ApplicationV2 options (window, position, classes, actions, etc.). |

Building a React actor sheet

Subclass ReactActorSheetV2, set reactApp, and register it as the sheet for your actor type. Override _prepareContext to choose exactly which props your component receives — this is also where you hand your component the [ContextConnector](#reacting-to-foundry-updates-with-contextconnector) so it can subscribe to live document updates:

import { ReactActorSheetV2 } from "foundry-vtt-react";
import MySheetApp from "./MySheetApp";

class MyActorSheet extends ReactActorSheetV2 {
  reactApp = MySheetApp;

  static DEFAULT_OPTIONS = {
    window: { title: "My Sheet", resizable: true },
    position: { width: 625, height: 750 },
    classes: ["my-sheet"],
  };

  async _prepareContext(options) {
    const context = await super._prepareContext(options);
    // Pick the props your React app receives:
    context.initialProps = {
      actor: context.document,
      source: context.source,
      contextConnector: this.contextConnector, // for live updates
    };
    return context;
  }
}

Register it like any other sheet, e.g.:

foundry.documents.collections.Actors.registerSheet("my-module", MyActorSheet, {
  types: ["character"],
  makeDefault: true,
});

Reacting to Foundry updates with ContextConnector

Every React application instance owns a ContextConnector at this.contextConnector. On every render, the mixin calls contextConnector.publishContext(context) with the prepared context (which, for a sheet, includes the updated document). Pass the connector into your component (via initialProps, as shown above) and subscribe to those updates so React re-renders when the Foundry document changes.

onUpdate(callback) returns a disposer function, ideal for useEffect cleanup:

import { useEffect, useState } from "react";

function MySheetApp({ actor: initialActor, contextConnector }) {
  const [actor, setActor] = useState(initialActor);

  useEffect(() => {
    // Re-render whenever Foundry re-renders the sheet
    const off = contextConnector.onUpdate(({ document }) => {
      setActor(document);
    });
    return off; // unsubscribe on unmount
  }, [contextConnector]);

  return <h1>{actor.name}</h1>;
}

You can debounce noisy update streams with Foundry's helper:

const handleUpdate = foundry.utils.debounce(({ document }) => {
  setActor(document);
}, 200);
const off = contextConnector.onUpdate(handleUpdate);

ContextConnector<T> API

| Method | Returns | Description | | ------------------------- | ------------ | ------------------------------------------------------------------- | | onUpdate(cb) | () => void | Subscribe to context updates. Returns a disposer that unsubscribes. | | tearDown(cb) | void | Unsubscribe a callback previously passed to onUpdate. | | on(event, cb) | () => void | Subscribe to a custom event. Returns a disposer. | | off(event, cb) | void | Unsubscribe a callback from a custom event. | | publishContext(context) | void | Emit a context update. Called for you by the mixin on each render. |

Use the returned disposer or tearDown(cb) to clean up — either removes the listener.

Development setup with Vite

For a fast dev loop with React Fast Refresh inside Foundry, add the foundry-vtt-react/vite plugin. Your manifest's esmodules points at dist/main.js: in production that's your built bundle, and in dev the plugin's middleware serves that same URL with the Fast Refresh preamble + a dynamic import of your real entry — so there's no shim file and no hand-written Vite config to maintain.

my-module/
├─ module.json          # esmodules: ["dist/main.js"]
├─ vite.config.ts
└─ src/
   ├─ main.ts           # real entry: Hooks, registerSheet, … (build input)
   └─ MySheetApp.tsx    # your React component(s)

1. Add the plugin — it derives everything from your module.json id:

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import foundryReact from "foundry-vtt-react/vite";

export default defineConfig({ plugins: [react(), foundryReact()] });

2. Add scripts"dev": "vite" for the HMR server, "build": "vite build" for production.

3. Make the module visible to Foundry — symlink (or copy) your module folder into Foundry's Data/modules/. With Docker, mount it instead:

foundry:
  image: felddy/foundryvtt:13
  volumes:
    - /path/to/my-module:/data/Data/modules/my-module

4. Run it — start Foundry (:30000), then npm run dev and open the Vite server (:30001). Edits to your components hot-reload. For production, npm run build ships dist/; the dev middleware isn't involved.

vite and @vitejs/plugin-react are optional peer dependencies — needed only for this plugin, not the runtime classes (you already have them for any React + Vite setup).

Plugin options

All options are optional:

| Option | Default | Description | | --------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------ | | appId | the id read from ./module.json | Your module's id. Used to build served paths and the proxy rule. | | entry | "src/main.ts" | Your real app entry / build input, relative to the project root. | | foundryUrl | "http://localhost:30000" | The local Foundry server that non-bundle requests are proxied to. | | port | 30001 | The Vite dev server port. | | manifestEntry | basename of module.json esmodules[0], else "main.js" | The bundle filename Foundry requests (where the dev preamble is served). |

What it expands to

For a module whose id is my-module, foundryReact() contributes the Vite config you'd otherwise hand-write — each value applied only when you haven't set it yourself, so your own config always wins:

{
  base: "/modules/my-module/dist",
  root: "src",
  // react/react-dom forced to a single copy — without this, a linked or git-installed
  // foundry-vtt-react pulls a second React and every hook throws "Invalid hook call".
  resolve: { dedupe: ["react", "react-dom"] },
  server: {
    port: 30001,
    proxy: {
      "^(?!/modules/my-module/dist)": "http://localhost:30000", // non-bundle requests → Foundry
      "/socket.io": { target: "ws://localhost:30000", ws: true },
    },
  },
  build: {
    outDir: "<root>/dist",
    emptyOutDir: true,
    rollupOptions: {
      input: "<root>/src/main.ts", // your `entry`
      output: { entryFileNames: "[name].js", assetFileNames: "[name].[ext]", format: "es" },
    },
  },
}

In dev it also serves the manifest URL (/modules/my-module/dist/main.js) with the module that replaces the old src/main.js shim:

// Fast Refresh preamble (reused from @vitejs/plugin-react; base derived from your config)
import { injectIntoGlobalHook } from "/modules/my-module/dist/@react-refresh";
injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;
import("/modules/my-module/dist/main.ts"); // dynamic, so the preamble runs first

Migrating from devSetup: devSetup is deprecated. Delete your src/main.js shim and the hand-written base/server/build config, then add foundryReact() — it reproduces the same behavior, deriving the preamble and paths from your resolved Vite config.

Exports

import {
  ReactApplicationV2,
  ReactActorSheetV2,
  ContextConnector,
  devSetup,
} from "foundry-vtt-react";