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

@reactre/compat

v0.0.4

Published

React 19's API on reactre's templates: alias react and react-dom to it and a React app runs unchanged.

Readme

@reactre/compat

Run a React 19 app on reactre's templates, unchanged. Function components and hooks, context, memo, refs, portals, Suspense and lazy, transitions, synthetic events, controlled fields and hydration all work as they do in React, and libraries such as React Router, TanStack Query, Radix, framer-motion and Recharts run on it (some of them through the class build below).

bun add @reactre/compat

Alias react, react-dom and their subpaths to it at build time. @reactre/plugin does it for Vite, Rollup, Rolldown, esbuild, webpack, Rspack and Bun, and compiles your JSX's HTML element trees into templates as well.

Vite

bun add -d @reactre/plugin @vitejs/plugin-react
// vite.config.ts
import reactre from "@reactre/plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({ plugins: [reactre({ compat: true }), react()] });

@vitejs/plugin-react brings Fast Refresh: save a component and it renders again in place, keeping its state, as with React. An edit that changes a component's hooks mounts it anew, and so does any edit to a class component, as React does. Fast Refresh is in compat's development build alone (dist/dev), which the development export condition resolves, as Vite's dev server does; the production build has none of its code, so its size and speed are as they were. @vitejs/plugin-react is optional: without it, Vite reloads the page when you save.

Without the plugin, an alias is enough. Vite then compiles JSX for React's automatic runtime on its own:

export default defineConfig({
  resolve: {
    // react, react-dom and every subpath of them, in your code and in your dependencies; the
    // server renderer first.
    alias: [
      { find: /^react-dom\/(server|static)(\.(browser|edge|node|bun))?(\.js)?$/, replacement: "@reactre/compat/server" },
      { find: /^react(-dom)?(\/.*)?$/, replacement: "@reactre/compat" },
    ],
  },
});

Other bundlers

The plugin has an entry for each: @reactre/plugin/rollup, /rolldown, /esbuild, /webpack, /rspack and /bun, each called as reactre({ compat: true }). See its docs.

Modules

| Module | What it is | | ------------------------------------------ | ------------------------------------------------------------------------------------------ | | @reactre/compat | React's client API, whole: core and the feature modules | | @reactre/compat/core | The slim base: React's client API without the feature modules | | @reactre/compat/features/classes | Class components (lifecycles, setState, error boundaries), installed when loaded | | @reactre/compat/features/stacks | React's component stacks in error reports, installed when loaded | | @reactre/compat/features/keys | React's handling of a list whose keys repeat, installed when loaded | | @reactre/compat/features/retry | React's retry of a render that throws, installed when loaded | | @reactre/compat/features/concurrent | React's transition lanes, installed when loaded | | @reactre/compat/features/viewTransitions | React's view transitions (needs the lanes, and loads them), installed when loaded | | @reactre/compat/features/undo | React's handling of an urgent render that suspends with no boundary, installed when loaded | | @reactre/compat/classes | @reactre/compat under its earlier name, for apps that resolve to it | | @reactre/compat/server | react-dom/server: React's server rendering, on compat's hooks | | @reactre/compat/static | react-dom/static: the same module, picked by react-dom/static's conditions | | @reactre/compat/server.browser | react-dom/server.browser: the server with React's minified error codes | | @reactre/compat/server.edge | react-dom/server.edge and .bun: the codes from renderToString alone | | @reactre/compat/server.node | react-dom/server.node: the server with React's full error messages | | @reactre/compat/compiler | The compiler @reactre/plugin runs, and a Bun plugin of its own | | @reactre/compat/api | The keys that switch React's APIs and behaviours on and off, and their rules |

@reactre/compat is all of React's client API. The plugin builds narrower entries from the same modules: with compat: { classes: false }, or any key in compat.exclude, react resolves to a module built on @reactre/compat/core that loads each feature module still on, so every set of options behaves as React does for everything it keeps. Without class support, a class that renders throws an error naming the fix. Without the componentStack key, errors are reported with an empty componentStack; without repeatedKeys, a list whose keys repeat, which React warns about, is reconciled by compat's own rules; without renderRetry, an error a render throws goes to the nearest error boundary or the root at once, and nothing is reported as recoverable. See the plugin's docs. @reactre/compat/core alone, with no feature module, is for builds that choose all four off, as the js-framework-benchmark app does. The compiler is optional; without it, everything still runs.

Every change is checked against React 19 itself, scenario by scenario: the same app runs on both, and what each renders and logs must match line for line. Errors are React's production errors too: the client throws and reports the minified codes React's production client does (Minified React error #31; visit https://react.dev/errors/31?args[]=…, the full message one click away), and the full messages where React keeps them, as in Children.

Where the client differs

  • Component stacks (componentStack in what onUncaughtError, onCaughtError, onRecoverableError and componentDidCatch get) are React's production stacks: host elements and boundaries by name, and each function and class component by the frame of a native stack trace, found as React's describeNativeComponentFrame finds it, by calling or constructing the component without props when the error is reported (use() of a promise throws then, as React's does with no dispatcher). One case differs: under JavaScriptCore (Bun, Safari), a component that returns a hook's result directly, a tail call, has no frame of its own, so the frame shown is the hook's, in compat's code rather than React's.
  • Invalid props throw React's errors (#60, #61, #62, #91, #137, #284) where React's client throws them: a mount's as the element renders, an update's as it commits, after the whole render, so the rest of the tree renders and commits, and the nearest boundary gets the error, with a stack from the element, once the commit's effects ran. One case differs: an update that gives a void element children that render no node (false, [], a component that renders nothing) does not throw.
  • Repeated keys: compat matches a list's rows against the old ones as React's production build does, down to the old row React's map hides staying in the DOM, never rendered again. Before building a map, compat matches what it can cheaply by position (rows that line up, one run of removed rows, two rows that traded places, the changes a compiled list reports); where a key repeats, the keys feature module sends a list those paths would take to React's algorithm. Without that module (repeatedKeys off, or @reactre/compat/core alone), such a list is matched by compat's own rules.
  • Hooks are checked as React's production client checks them: a render that calls fewer hooks than the last (#300) or more (#310; #467 in a later pass of a first render, and #312 in a later pass on the server, where compat counts effects as hooks and React's server does not), or a state or optimistic hook where the last render called a hook of another kind (#311). A render that calls no hook is not compared, as in React: the next one mounts its hooks anew. A hook of another kind that React does not check, such as a ref where state was, reads what compat kept in the slot, which is not always what React kept.
  • A render that throws an error renders again before the error counts, as in React's production client (with @reactre/compat/features/retry, the renderRetry key): the root's first render renders again whole, and an update renders again the component that threw (or the boundary that caught, with its update undone), and only an error that render throws too is caught or reported. If it renders, the first error goes to onRecoverableError as #520, with the stack it was first captured with. A throw captures the component stack twice, as React does, so React's calls of each component without props to read its frame match too. Where it differs:
    • Only an error a component's render throws renders again. An error from an element's props, a child's type, a class lifecycle, lazy or a context consumer counts at once. React checks an update's props as it commits them, so for those it does the same; on a mount it renders the root again first.
    • In an update, React renders the whole root again, and compat only the component that threw, after the boundary above renders its fallback for show. The components between the boundary and the root render once, and elements a parent passed down from above are not rendered again.
    • The retry does not compute useMemo again.
    • Hydration does not render again: it recovers as described below.
  • getSnapshotBeforeUpdate is called as in React: after the whole render, children first, with the DOM as it was before the render (compat writes the DOM as it renders, so it undoes the render's changes around the calls and makes them again). Without a MutationObserver it is called before its class renders; once per update either way, however many times the update renders.
  • A root's first render whose shell suspends, with no Suspense boundary above, renders (or hydrates) once more, as React's prerender does. An update outside a transition that suspends with no boundary above commits what rendered, and renders the root again when the wait settles; React keeps the screen as it was, prerenders, and renders the update then (telling it apart would cost every render a snapshot).
  • Transitions render as React's lanes do (with @reactre/compat/features/concurrent, the concurrentRendering key): their updates wait and render in a task after urgent ones, replayed in order over the urgent updates made since; isPending renders true first, on React's sync lane; transitions started in different events render one after the other, and those that change the same state together; updates made in an async action wait for it; useDeferredValue renders the new value in a background transition; a store a transition read that moved meanwhile renders it again before it commits. A transition that suspends where content already shows (a boundary that shows it, or no boundary) commits nothing: the screen stays, isPending stays true, and it renders again when the data comes or another update arrives. Compat gets there by rendering the transition in place and undoing it: a MutationObserver records its DOM changes, its roots' fibers are saved, and the effects, cleanups and ref detaches it would run wait for the commit. Where it differs:
    • No time slicing: a transition renders whole in its task, so an urgent update that arrives while it renders waits for it. A render shorter than React's 5 ms slice looks the same. @reactre/full-compat slices as React does, at about 1 KB gzipped and a clock check before each component, in every render.
    • Render counts match React's in an attempt that suspends (the boundary's other children and its fallback render too), when a suspended transition renders again after another commit, and for a component whose state its transition leaves as it was (it renders, its children do not). Which of two transitions waiting at once renders first follows the transition lane each got, from React's cycle of 10 shared by the page, and an urgent update that sets state to the value it has while a transition waits to change it renders the component, which then bails out, as in React.
    • Content a boundary can show again within 300 ms of the last fallback appearing renders at once and commits when the 300 ms end, as React's throttled reveal: its DOM changes are undone and its fibers put back until then, and an update that comes first drops the render, which renders again. Where the render cannot be undone (a root that hydrates, or no MutationObserver), compat waits, then renders.
    • Undoing puts back attributes, text and nodes, the document metadata the render hoisted to <head> (not what other code put there), the value and checkedness of inputs and textareas, and the options a select had selected. Scroll positions stay too, as no layout runs between the render and its undo, except an element's own that the render removed from the document or moved, which the browser resets as the element leaves it (Chrome). A root that is hydrating, or a page without MutationObserver, renders its transitions without undo, showing fallbacks as an update outside a transition does.
  • ViewTransition works as in React 19.3.1's client (with @reactre/compat/features/viewTransitions, the viewTransitions key): a transition, or content a Suspense boundary reveals, that changes a <ViewTransition> commits through document.startViewTransition, with the transition's types (addTransitionType), React's view-transition-name and view-transition-class on the elements of what leaves, enters, updates or pairs by name, the root's own transition cancelled when nothing outside changed, and onEnter, onExit, onUpdate and onShare called once it is ready, their cleanups once it finishes. A commit made while one runs waits for it to finish; an update before its update callback skips it and commits at once. Compat holds the render as it holds a throttled reveal: undone, its names put on the old DOM, then made again in the update callback. Without the API the transition commits plainly, as React's does. Where it differs: React's passive phase clears the names of elements it never named, which leaves an empty style attribute on them, and compat does not; which boundaries changed is worked out from the fibers saved before the render rather than from React's flags, so an exotic change React flags differently can name a boundary React does not. SuspenseList is not exported, as React 19.3.1's stable react does not export it; an element made with its symbol (Symbol.for("react.suspense_list")) renders its children in order without coordinating when they reveal, where React's client coordinates them.
  • Hydration does what React's production client does. The shell hydrates in a task (at once inside flushSync), and each Suspense boundary and Activity it meets stays dehydrated, the server's HTML in place, until a later pass hydrates it, nested boundaries a pass after the ones around them, with React's order of renders, effects and reports. A boundary whose content suspends keeps the server's HTML and hydrates once the content can render; one still streaming in (<!--$?-->, <!--$~-->) waits for the server, through _reactRetry and the document's load, and one the server could not finish (<!--$!-->) renders on the client first, with React's error. A click or another discrete event inside a dehydrated boundary, or before the shell hydrated, hydrates it first, outermost first, then reaches its handlers; while its target cannot hydrate, it reaches none. The latest continuous event of each kind (focusin, dragenter, mouseover, and pointerover and gotpointercapture for each pointer) whose target has not hydrated is kept, what blocks it hydrates before other boundaries, and it is dispatched again once nothing does. Claimed elements keep the server's attributes and styles until a prop changes, and take only what React's hydrateInstance applies: listeners, and an input's or textarea's initial value, checkedness and name. Where the server's markup does not match (an element, text or boundary the server did not render, server nodes the client does not render, text that differs), and for any other error thrown while hydrating, the nearest Suspense boundary or Activity the server rendered, or else the root, renders on the client, and onRecoverableError gets React's error (#418, or #422 or #423 caused by the error) and component stack. A whole document that renders on the client, or that unmounts after an uncaught error, keeps its <html>, <head> and <body>, and the scripts, styles and stylesheets in them, as React's singletons do. useSyncExternalStore hydrates with getServerSnapshot, and without one renders on the client with React's error (#407, as the cause of #422 or #423); once it subscribes, a store whose snapshot is not the server's renders again. Where it differs:
    • An update that reaches a boundary still dehydrated hydrates the boundary first, with the props and context it had, in a commit of its own, then renders. Before each such boundary hydrates, the updated components render as far as it, a render that is then undone, as React renders them and drops that render (with @reactre/compat/features/concurrent; without it, or without MutationObserver, that render is left out). In a transition, content that cannot hydrate yet keeps React's old UI until it can; compat, which does not undo a transition on a root that hydrates, renders that boundary on the client, as React does for any other update.

Server rendering

@reactre/compat/server is react-dom/server (and react-dom/static, and their .browser, .edge, .node and .bun builds) for compat apps; the plugin resolves them to it, each build to the entry that stands for it. renderToString, renderToStaticMarkup, renderToReadableStream and renderToPipeableStream write the HTML React 19.3.1's production build writes for the same tree, byte for byte: text separators, Suspense markers and the scripts that stream suspended content in, hoisted <title>, <meta> and <link> tags, image preloads, the links and scripts of preload, preinit and the other resource functions (or the Link header onHeaders gets), stylesheet precedence, form actions and useId's ids. Components call compat's own hooks, effects never run, and class components render whether or not the class build is loaded.

Server Components stay on React: the plugin leaves the RSC environment, and any build resolving with the react-server condition, on real React. Where compat differs from React's server renderer:

  • Partial prerendering is React's: prerender, prerenderToNodeStream, resumeAndPrerender and resumeAndPrerenderToNodeStream (react-dom/static) write React's prelude and its postponed state, the same JSON, so compat resumes React's prerenders and React resumes compat's; resume and resumeToPipeableStream write the rest. The web entries enqueue as React's browser build and hash $ACTION_KEY with MurmurHash3, the Node stream ones as its Node build, with MD5. React 19.3.1's stable builds have no onPostpone or unstable_postpone: only an abort postpones work, and an onPostpone option is ignored, as React ignores it. Where React's own partial prerendering fails, compat fails the same way: a resumeAndPrerender aborted inside a boundary it resumed fails with React's "It should not be possible to postpone at the root" error, and a resume of SuspenseList rows that reveal in order or together never resolves allReady.
  • preload, preinit and the other resource functions called in async work a stream's render started, such as a promise callback, reach that render wherever AsyncLocalStorage exists (a global one, as edge runtimes have, or node:async_hooks'), as in React's Node build and its edge build. Under Bun, react-dom/server resolves to React's Bun build, which keeps no such storage and drops those hints; compat takes them there too, as react-dom/server.node does under Bun. Without AsyncLocalStorage, only code the render's work runs reaches it, as in React's browser build.
  • useActionState's server action key ($ACTION_KEY) hashes the hook's place in the tree as each React build does for the entry: MD5 for renderToPipeableStream, as React's Node build; MurmurHash3 for renderToReadableStream and renderToString, as its browser, edge and legacy builds. React's Node build hashes its renderToReadableStream's keys with MD5 too, and its Bun build hashes with Bun.hash; a postback still matches the server that wrote the form. (unstable_externalRuntimeSrc is ignored, as React's production build ignores it.)
  • Errors are those of React's build for the entry. React's browser builds (react-dom/server.browser and react-dom/static.browser, which a bundle for the browser or a web worker resolves react-dom/server to) throw its minified production codes (Minified React error #62; visit https://react.dev/errors/62 …), and so does @reactre/compat/server.browser. Its Node build throws full messages, and so does @reactre/compat/server (and /server.node). Its edge and Bun builds throw full messages from the streams and minified codes from renderToString and renderToStaticMarkup, which they take from the browser build, and so does @reactre/compat/server.edge. @reactre/compat/server and @reactre/compat/static pick among the three with react-dom's export conditions (browser and worker the browser entry, workerd, edge-light and, for /server, bun the edge one, and for /static, deno the browser one), so a bundle for the browser gets the codes and a Node server the full text, as with React. Each entry exports the whole API, where React's browser and edge builds leave out the Node stream functions, and the browser entry's Node stream functions throw the codes too.
  • onError's componentStack is React's production stack, each component's frame read from a native stack trace as React reads it, with two exceptions. A component whose first call that throws without props is use() of a promise gets a frame with only its name, as compat's use() reads a promise outside a render where React's throws. Under JavaScriptCore (Bun, Safari), a component that returns a hook's result directly, a tail call, has no frame of its own, so the frame shown is the hook's, in compat's code rather than React's. Frames are formatted as React's browser, Bun and legacy builds format them; its Node and edge builds use an Error.prepareStackTrace of their own, which Bun writes differently for an engine-made frame such as a class's implicit constructor.
  • Stream chunks are those of one React build per entry: renderToPipeableStream writes as React's Node build does (4096-byte views, a long string as it is, and a pause when the writable is full until it drains), renderToReadableStream enqueues as its browser and edge builds do (2048-byte views). React's Bun build, which react-dom/server resolves to under Bun, writes each piece on its own instead. The bytes are the same.

Size

Each module bundled whole, minified, for the browser in production mode, with React 19.3.1 for comparison:

| Bundle | Minified | gzip | brotli | | ----------------------------------- | -------: | ------: | ------: | | @reactre/compat | 120.4 KB | 44.0 KB | 39.0 KB | | @reactre/compat/core | 103.9 KB | 38.0 KB | 33.6 KB | | react + react-dom/client 19.3.1 | 214.1 KB | 66.7 KB | 57.4 KB |

The feature modules cost an app about 29.6 KB minified, 10.9 KB gzipped: class support 1.6 KB gzipped, React's transition lanes 3.2 KB, React's view transitions 3.6 KB, component stacks 1.1 KB, React's render retry 0.8 KB and React's handling of repeated keys 0.4 KB, each left out with its key off. @reactre/compat/core holds the code of the keys, the retry, the lanes and the view transitions' commit, which a bundle that does not load their modules leaves out. Most of the runtime ships whatever an app imports: a counter with one useState and createRoot comes to 31.4 KB gzipped (20.5 KB on core alone), against 64.4 KB on React. Measured with Bun's bundler, gzip at level 9 and brotli at quality 11; Rolldown, the bundler in Vite, gives sizes within 1 KB of these.

License

Apache-2.0