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

@forge-go/client-react-devtools

v1.13.2

Published

Mount the Forge client devtools from React, and pay nothing for it in production

Readme

@forge-go/client-react-devtools

Mount @forge-go/client-devtools from React, and pay nothing for it in production.

You've probably wired the devtools by hand before:

if (process.env.NODE_ENV !== 'production') {
  void (async () => {
    const { attach } = await import('@forge-go/client-devtools');
    const { mountOverlay } = await import('@forge-go/client-devtools/overlay');

    globalThis.forge = attach(client);
    mountOverlay(globalThis.forge);
  })();
}

Two imports, because attach and mountOverlay live at two entry points: the inspection API is the root, the UI is /overlay (or /panel). Reaching for mountOverlay on the root import gets you undefined, and written as an optional call it fails silently.

This package replaces that block with a component:

<ClientProvider client={client}>
  <Routes />
  <ForgeDevtools />
</ClientProvider>

ForgeDevtools resolves the cache through useClient(), the same explicit, then provider, then global precedence your other hooks already follow. On mount it attaches the inspector, mounts the panel, and sets globalThis.forge so you can drive it from the console. On unmount it tears all of that down. It renders null: the panel lives in its own shadow root on document.body, so your re-renders can't disturb it and your CSS can't reach it.

Not a subpath on @forge-go/client-react. That package is a production dependency of your app, and hanging a devtools subpath off it would drag a client-devtools peer into a production manifest. A separate package keeps client-devtools out of that manifest, which is where the constraint actually bites. This package does declare it as a peer, because ForgeDevtools imports it at runtime, but this package is itself a devDependency of your app, so the peer it pulls in is one too.

Install

Four packages, all of them dev-only from your app's point of view:

npm i -D @forge-go/client-react-devtools @forge-go/client-devtools

@forge-go/client-core and @forge-go/client-react are the other two peers, and you already have them: they are what your app is built on. Skipping @forge-go/client-devtools installs cleanly and then throws ERR_MODULE_NOT_FOUND the first time <ForgeDevtools /> mounts, because that is when it is dynamically imported.

Props

| prop | default | | |---|---|---| | client | none | Beats the provider and the module default, same as everywhere else. | | panel | true | false mounts the lean overlay instead of the full panel. | | open | false | Start open rather than as a button in the corner. | | frames | off | Keep the last N stream frames, payloads included. | | limit | 500 | How many events the causal log holds. | | manager | from cache.live | Only needed if your app built a SubscriptionManager the cache doesn't know about. | | binder | from cache.live | Same escape hatch as manager, for a StreamBinder. |

useForgeDevtools(client?) gets you the same Devtools instance from a hook, for a test or a component that wants to call whyNotRefetched directly instead of going through the panel.

Zero production cost, from two directions

The package's exports map does the first one:

"exports": {
  ".": {
    "types": "./dist/index.d.ts",
    "development": "./dist/dev.js",
    "default": "./dist/noop.js"
  }
}

A bundler that honours the development condition (Vite and webpack 5 both do; esbuild wants --conditions=development) resolves import and require calls straight to dist/noop.js in a production build. That file is one function returning null. There's no devtools code in the bundle to tree-shake away, because none of it was ever resolved in the first place.

The second guard is for a bundler that ignores export conditions and resolves dist/dev.js regardless. Inside it, a bare process.env.NODE_ENV !== 'production' wraps the dynamic imports of client-devtools, ./panel and ./overlay. Written bare, that check folds at build time and the import along with it, so a minifier drops the whole branch. Spelt defensively, as typeof process === 'undefined' || process?.env?.NODE_ENV !== 'production', the first half can't fold, the guard survives, and the whole package ships to production with it. client-devtools's own README carries the same warning, for the same reason: it's cheap to write the safe-looking version and expensive to find out later that it never folded.

StrictMode

attach() claims cache.observer, a single slot on the cache, and hands it back on dispose(). React 18 StrictMode double-invokes effects in development, which is exactly where ForgeDevtools runs. Mount, mount, unmount, unmount against one observer slot would either leave two panels on screen or restore a stale observer over a live one, and neither is a bug you want to chase at 11pm.

So attachment is a module-level map, keyed by cache, refcounted per component. A second <ForgeDevtools /> mounted against the same cache joins the first instance instead of attaching a second one, and the observer only comes back once the last holder has unmounted. Two components mounting in the same tick both await the same dynamic import; whichever resolves second finds the first one's entry already there and joins it too.