Skip to content

franzwilding/react-htx

Repository files navigation

⚡️ react-htx

Proof of Concept – This project is still experimental and not ready for production.

react-htx lets you write React components directly in HTML — making it possible to render and hydrate a React app using server-generated HTML from any backend (e.g. Symfony/Twig, Rails, Laravel, Django, etc.).

✨ Instead of manually wiring React components everywhere, just return HTML from your backend and react-htx will transform it into a live, interactive React application.

It even includes a built-in router that intercepts link clicks and form submissions, fetches the next page via AJAX, and updates only what changed — keeping React state intact between navigations.


🚀 Features

  • 🔌 Backend-agnostic – Works with any backend (Symfony, Rails, Laravel, etc.)
  • 🛠 Use existing backend helpers (Twig path functions, permission checks, etc.)
  • 🔄 State preserved across pages – No resets on navigation
  • 📋 Form support – Modify forms dynamically (e.g., add buttons on checkbox click) without losing state or focus
  • 🪶 Lightweight – Just a few lines of setup, no heavy dependencies

📦 Installation

npm install react-htx

Since react and react-dom are peer dependencies, make sure to also install them:

npm install react react-dom

💡 Usage

Your backend returns simple HTML:

<html lang="en">
  <body>
    <div id="htx-app">
      <h1>Hello world</h1>
      <ui-button type="primary">This will be a shadcn button</ui-button>
    </div>
  </body>
</html>

Your frontend mounts the react-htx app:

// app.ts
import loadable from '@loadable/component'
import { App } from 'react-htx'

const component = loadable(
  async ({ is }: { is: string }) => {
    return import(`./components/ui/${is.substring(3)}.tsx`)
  },
  {
    cacheKey: ({ is }) => is,
    // Since shadcn files don’t export a default,
    // we resolve the correct named export
    resolveComponent: (mod, { is }: { is: string }) => {
      const cmpName = is
        .substring(3)
        .replace(/(^\w|-\w)/g, match => match.replace(/-/, '').toUpperCase())
      return mod[cmpName]
    },
  }
)

// Uses the HTML element with id="htx-app" as root
new App(component)

🎨 Example with Custom Root Component & Selector

// app.ts
import loadable from '@loadable/component'
import { App } from 'react-htx'
import { AppProvider } from './providers/app-provider.tsx'

const component = loadable(
  async ({ is }: { is: string }) => import(`./components/${is}.tsx`),
  { cacheKey: ({ is }) => is }
)

new App(component, AppProvider, '#app')
// providers/app-provider.tsx
import React, { ElementType } from "react"
import { App, RootComponent } from "react-htx"
import { RouterProvider } from "react-aria-components"
import { ThemeProvider } from "./theme-provider"

export const AppProvider: React.FC<{
  app: App
  element: HTMLElement
  component: ElementType
}> = ({ app, element, component }) => (
  <React.StrictMode>
    <RouterProvider navigate={app.router.navigate}>
      <ThemeProvider>
        <RootComponent element={element} component={component} />
      </ThemeProvider>
    </RouterProvider>
  </React.StrictMode>
)

🔄 Navigation Without Losing State

When navigating, react-htx fetches the next HTML page and applies only the differences using React’s reconciliation algorithm. 👉 This means component state is preserved (e.g., toggles, inputs, focus).

<!-- page1.html -->
<div id="htx-app">
  <h1>Page 1</h1>
  <ui-toggle json-pressed="false">Toggle</ui-toggle>
  <a href="page2.html">Go to page 2</a>
</div>
<!-- page2.html -->
<div id="htx-app">
  <h1>Page 2</h1>
  <ui-toggle json-pressed="true">Toggle</ui-toggle>
  <a href="page1.html">Go to page 1</a>
</div>

Only the <h1> text and the pressed prop are updated — everything else remains untouched ✅.


Props

If you pass props to your htx components like this:

<my-component enabled name="test" data-foo="baa" as="{my-other-component}" json-config='{ "foo": "baa" }'

your components will get this props:

const props = {
    enabled: true,
    name: 'test',
    foot: 'baa',
    as: <MyOtherComponent />,
    config: { foo: 'baa' },
}

Slots

react-htx also provides a simple slot mechanism: Every child if a htx-component with a slot attribute will be transformed to a slot property, holding the children of the element:

<my-component>
    <template slot="header"><h1>My header content</h1></template>
    <div slot="footer">My footer content</div>
</my-component>

your components will get this props:

function MyComponent({ header, footer } : { header : ReactNode, footer : ReactNode }) {
    <article>
        <header>{header}</header>
        <div>My content</div>
        <footer>{footer}</footer>
        <aside>
            <footer>{footer}</footer>
        </aside>
    </article>
}

🤝 Contributing

Contributions are welcome! Feel free to open an issue or submit a PR.


🛠 Development Build

If you’re contributing to this library:

npm install
npm run build

About

Use HTML on the server to compose your react application.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published