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

reactolith

v1.2.1

Published

Use HTML on the server to compose your react application.

Readme

⚡️ reactolith

Hotwire Turbo for React. Render and morph a React app from server-generated HTML — Twig, ERB, Blade, Jinja, anything that prints strings.

reactolith lets you write React components directly in HTML so your backend stays in charge of routing, templates, permissions, and URLs, while React stays in charge of interactivity. It is not Inertia for the masses — it is a different approach: instead of shipping JSON page props and re-rendering a top-level component on every navigation, reactolith fetches the next HTML page, morphs the existing React tree in place, and preserves component state across navigation, form submits, and live server pushes.

📖 Full documentation: https://reactolith.github.io/


Why reactolith exists

I built reactolith because I wanted Hotwire Turbo — but for React.

Inertia.js is the closest thing in the React world, and it is great, but it is fundamentally a different model: Inertia replaces the page-level component on every visit. There is no equivalent of Turbo's morph — you cannot keep a sidebar's collapsed state, a video playing, or a half-typed form alive across a backend-driven navigation. The whole "page" is a single JSON-fed component that gets swapped out.

reactolith takes the Turbo approach instead. The server returns HTML, not JSON. reactolith parses that HTML, diffs it against the live React tree, and updates only what changed — using React's own reconciler. Component state, focus, scroll, open dialogs, mounted iframes: they all survive.

That tiny difference unlocks something bigger: it makes the Majestic Monolith a serious option again for teams who want React. You can keep one Rails / Symfony / Laravel / Django app — one router, one auth layer, one set of URL helpers, one deployment — and still ship a sticky, app-like React frontend on top of it. No SPA-shaped backend. No GraphQL gateway. No /api/v2 to keep in sync with the UI. Templates render HTML; reactolith makes that HTML interactive.

If you've ever looked at the Hotwire stack and wished you could use React components inside it, reactolith is that.


Install

npm install reactolith react react-dom

Requires Node 18+ and React 19.


A 30-second tour

<!-- index.html (rendered by your backend) -->
<div id="reactolith-app">
  <h1>Hello world</h1>
  <my-button>Click me</my-button>
</div>
// src/main.tsx
import { App } from "reactolith";
import { MyButton } from "./components/my-button";

new App(({ is }) => (is === "my-button" ? MyButton : null));

Any tag name with a hyphen (<my-button>) is resolved to a React component; everything else renders as a native element.

For larger apps, point createLoader at a folder of components and skip the per-tag wiring:

import { App, createLoader } from "reactolith";

new App(createLoader({
  modules: import.meta.glob("./components/ui/*.tsx"),
  prefix: "ui-",
}));

Highlights

  • 🔌 Backend-agnostic — works with any backend (Symfony, Rails, Laravel, Django, …)
  • 🔄 Morphing navigation — like Turbo's morph: the tree is diffed in place, React state is preserved across link clicks and form submits
  • 📋 Forms — modify forms dynamically (server can add/remove fields on a checkbox click) without losing input state or focus
  • 📡 Realtime — Mercure SSE pipes server-pushed HTML through the same render path
  • 🧠 Scroll restoration — browser-like behavior across SPA-style navigations
  • 🧩 IDE autocomplete — generate web-types for JetBrains/VS Code so <my-button> autocompletes like a native element
  • 🪶 No new backend layer — no JSON page-prop contract to maintain, no shadow API; your existing controllers and templates are the API

See the docs for the full feature list, API reference, and end-to-end guides.


Server-side rendering

For static-site generators or progressive enhancement, import a server-safe renderToString from reactolith/server:

import { JSDOM } from "jsdom";
import { renderToString } from "reactolith/server";
import { resolveComponent } from "./resolve-component";

const dom = new JSDOM(`<div id="root"><my-button>Hello</my-button></div>`);
const html = renderToString(
  dom.window.document.getElementById("root")!,
  resolveComponent,
);

Router and Mercure side effects are skipped on the server because both rely on useEffect. See the SSR guide for the full walkthrough.


Reactolith vs. the alternatives

If you came here looking for an alternative to one of these, head to the side-by-side comparisons:


Examples

End-to-end example apps live in /examples:

  • symfony-multistep-form — Symfony 8 FormFlow with a complete shadcn/ui form theme that covers every native Symfony form field type. The Twig view is one line ({{ form(form) }}); every visual decision lives in a single form theme file.

Docs map

| Topic | Link | |---|---| | Why reactolith | Comparisons | | Install & set up | Installation | | First app | Quick Start | | Mental model | How It Works | | Props & slots | Props · Slots | | Forms & validation | Forms | | Scroll | Scroll Restoration | | Realtime | Mercure | | SSR | Server-Side Rendering | | Tooling | Web Types · API Cheatsheet |

The docs site lives in /docs and is itself built with reactolith — every page is plain HTML hydrated into React.


Development

npm install
npm run build       # dist/index.{mjs,cjs}, dist/index.d.ts, CLI bundle
npm test            # vitest run
npm run typecheck   # tsc --noEmit
npm run lint        # eslint src tests

prepublishOnly runs npm run build so published artifacts always come from a fresh build.

Contributions welcome — open an issue or PR. See CONTRIBUTING.md for the local-checks workflow, docs-site setup, and commit conventions.