# react-to-web-component and the problem of sharing React without React

> r2wc wraps a React component in a custom element so plain HTML pages, or projects on Vue, Svelte, Angular, Ember or CanJS, can render it. It costs a small runtime and a couple of sharp edges around attributes and version compatibility.

**bitovi/react-to-web-component** — Convert react components to native Web Components.  Works with Preact too!

- Repository: https://github.com/bitovi/react-to-web-component
- Website: https://www.bitovi.com/open-source/react-to-web-component
- Stars: 948 · Forks: 52
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/bitovi-react-to-web-component

## Why wrap React instead of shipping the framework

The problem this project solves is narrow and real. A React component is a function that returns a description of UI, and rendering it normally requires a React runtime mounted above it in the tree. That assumption breaks in the places teams actually hit: a page template owned by a marketing system, a plugin for a CMS, an embeddable widget, or a codebase that moved to a different framework and still needs to show one legacy component.

`@r2wc/react-to-web-component` takes a React component and returns a `CustomElementConstructor`. The custom element becomes a wrapper that holds the React tree inside itself. Because the wrapper is a real custom element, a page that knows nothing about React can drop it into markup the same way it uses `div` or `img`.

The README lists the frameworks this works with, including vue, svelte, angular, ember and canjs, and also plain HTML projects. That list is the point of the project. The consumer does not need React, a build step for React, or any knowledge of JSX to render the element. It also means the component's own dependencies travel with it, so a component built on a heavier UI library still drags that library into the host page.

## The element does not render until it enters the page

The mechanism under the hood is worth understanding before you commit to it. According to the README, `r2wc` creates a `CustomElementConstructor` with custom getters and setters plus lifecycle methods that track the props you declared. When a property is set, the custom setter re-renders the React component inside the custom element.

Two details matter in practice. First, enumerable properties and values on the custom element are the ones used as the `props` passed to the React component, so prop names are the API surface of the element. Second, and this is the useful part for server rendering and for pages that build DOM piecemeal, the React component is not rendered until the custom element is inserted into the page.

There is a caching behaviour inside that setter too. After a property is set, the setter creates an enumerable getter and setter on the instance to save the value and avoid hitting the proxy in the future. The practical effect is that repeated writes do not each pay for a fresh lookup path, but it also means the element instance keeps its own copy of the last value. If your component mutates state internally and something outside writes to the same property, the two paths to the same value are worth checking before you assume a single source of truth.

## Attributes are opt-in, so plain HTML attributes silently do nothing

This is the sharpest edge in the basic flow and it produces no error at all. By default, custom elements created by `r2wc` only pass properties to the underlying React component, so writing `name="Justin"` in markup does nothing to the rendered output. The element renders, and the attribute is inert.

To make attributes work you must declare your component's props and give each one a type, for example `string`. Once the prop map is in place, the element looks for the matching attribute and binds it.

That design means the element has two distinct configuration surfaces, properties and attributes, and only one of them is live by default. For a component consumed from hand-written HTML, the prop map is the contract. For a component consumed from JavaScript, you can set properties directly on the element instance and skip attribute binding entirely.

The README points to two further documents for this kind of ground: a programmatic usage and declarative demo, and a complete example built around a third party library. It also links a sandbox titled All the Props, described as a demo of every prop transform type the project supports. That sandbox is the fastest way to understand the type vocabulary beyond `string` before you design a public element API.

## Installing the package and defining your first element

Installation is a single npm package. The README gives this as the way to install it:

```bash
npm install @r2wc/react-to-web-component
```

Take a simple component:

```js
const Greeting = () => {
  return <h1>Hello, World!</h1>
}
```

Call `r2wc` with the component and register the result with the custom element registry:

```js
import r2wc from "@r2wc/react-to-web-component"

const WebGreeting = r2wc(Greeting)

customElements.define("web-greeting", WebGreeting)
```

Now the element can be used from any HTML document:

```html
<body>
  <h1>Greeting Demo</h1>

  <web-greeting></web-greeting>
</body>
```

Two notes on that sequence. The registration name in `customElements.define` has to contain a hyphen, since the registry requires it, so `web-greeting` is valid where `webgreeting` would not be. And the element must be defined before the markup that uses it is parsed, otherwise the browser upgrades the unknown tag later and the component can appear after the rest of the page. If you bundle the definition in a separate script file, load it ahead of the markup that references it.

## Adding a props map so the attribute reaches the component

To make the element configurable from markup, declare the props. The README's example takes `name` as a prop and gives it the `string` type:

```js
const Greeting = ({ name }) => {
  return <h1>Hello, {name}!</h1>
}

const WebGreeting = r2wc(Greeting, {
  props: {
    name: "string",
  },
})
```

With that in place the attribute is picked up:

```html
<body>
  <h1>Greeting Demo</h1>

  <web-greeting name="Justin"></web-greeting>
</body>
```

Two things are worth flagging here. Attribute values in HTML are always strings, so a typed prop such as a number or a boolean will arrive as text and needs a converter on your side or a type the project handles for you. That is what the prop transform types in the All the Props sandbox are for, and what the API documentation is for.

Also note that this example declares one prop, `name`. Anything else the component expects must be declared too, or it stays a property only. The README describes `r2wc` as supporting all the prop transform types in that demo, but it does not enumerate them in the setup section, so treat the API documentation as the source of truth rather than guessing type strings.

## Shadow DOM method transforms were broken until the February 2026 release

The release history shows the project has been tightening its handling of shadow DOM. Version 2.1.0 of `@r2wc/react-to-web-component` was published on 2025-09-12 with the note "Add method property converter." Both the current `@r2wc/react-to-web-component` v2.1.1 and `@r2wc/core` v1.3.1 were published on 2026-02-25 with the same note, "Fix method transforms when shadow DOM is used."

Two separate packages are being versioned in step, one for the React wrapper and one for the core it sits on, so a version bump in one usually means the other moved as well. If you pin versions, pin both. A mismatch across the two packages is a plausible source of subtle runtime differences.

The nature of that fix matters for adopters. A method transform lets a prop hold a function that the React component can call, which is the mechanism you need for things like a submit callback or a ref to an inner chart instance. That mechanism was not behaving correctly when the element rendered into a shadow root until the February 2026 patch. If your consumer application attaches the element to a shadow DOM, stay on the 2026-02-25 build or later and confirm the callback path works in that context rather than assuming it does.

## Where r2wc is the wrong choice

There are cases where this is the wrong tool, and the project says so plainly. The README carries a note that the latest version only works with React 18, and that users on React 16 or 17 should use version 1. That is a hard version boundary, not a soft warning, so an organisation with a mixed React estate either standardises on 18 or keeps two copies of the package in play.

The wrapper is also the wrong tool inside an application that already runs React. If the host page has a React root, rendering a nested React tree inside a custom element means two renderers, two copies of hooks context, and a bridge for state and events between them. The framework-to-framework section of the README is about getting a React component into a project that does not use React.

Browser support is described as covering all modern browsers, with Edge needing a `customElements` polyfill. That is a real deployment constraint for older internal browsers or long life enterprise environments.

Size is the last consideration, and it is the smallest of them. The README states the package is 1.36KB minified and gzipped. That is the library overhead, not the size of the React runtime it carries with it.

## Maintenance signals, licence, and where to look next

The repository is not archived, and the last push was on 2026-08-02, with releases landing on 2026-02-25 and 2025-09-12 for the packages described above. The project is built with TypeScript as its primary language, and the repository root shows an Nx workspace: `nx.json`, a `packages/` directory, `docs/`, `tests/`, and separate tsconfig files for the base, node and react projects. The root `package.json` is private and marked as a workspace root, with scripts that run Nx across many targets including `build`, `typecheck`, `eslint`, `prettier` and `depcheck`.

The licence field on the repository says MIT. That covers the wrapper library. It does not extend to the React runtime it depends on, or to the components you publish through it, which carry their own licences. The README does not document a policy for versioning the generated elements or for the semantic version of the custom element contract you expose, so if you are shipping elements to external teams, treat your own prop map as a public API and version it yourself.

For the pieces the README only points at, the API documentation file, the programmatic usage and declarative demo, and the complete third party example are the places to check before committing to a design.

## Conclusion

Adopt r2wc when a React component has to live inside markup you do not control, such as a CMS template, a legacy page, or a component owned by another team. Do not adopt it if the consuming app already runs React, since a shared bundle and a duplicated framework runtime cost more than the wrapper saves, and the props map adds a maintenance layer of its own. Check first that your React version is 18, because the current release line does not work with React 16 or 17, and read the props and attribute behaviour in the API documentation before assuming a property will bind as a plain HTML attribute.

## FAQ

### What is react-to-web-component used for?

It converts React components into custom elements so they can be used in plain HTML pages or in projects built with vue, svelte, angular, ember and canjs. The custom element wraps the React component so it does not have to be mounted through React.

### How do I install react-to-web-component and use it for the first time?

Run `npm install @r2wc/react-to-web-component`, pass your component to `r2wc`, then register the result with `customElements.define` using a name that contains a hyphen. The element can then be placed directly in HTML markup.

### Why is my attribute not reaching the React component?

By default the custom element only passes properties to the React component. To make attributes work you must declare your props and their types in the options passed to `r2wc`, for example `props: { name: "string" }`.

### Does react-to-web-component work with React 16 or 17?

Not with the latest version. The README states that the latest version of the package only works with React 18, and that React 16 or 17 users should use version 1.

### How large is the package and what does it require?

The README states the package is 1.36KB minified and gzipped and works in all modern browsers, with Edge needing a `customElements` polyfill.

## Sources

- [bitovi/react-to-web-component on GitHub](https://github.com/bitovi/react-to-web-component)
- [License: MIT](https://github.com/bitovi/react-to-web-component/blob/main/LICENSE)
- [Project website](https://www.bitovi.com/open-source/react-to-web-component)
- [README](https://github.com/bitovi/react-to-web-component/blob/main/README.md)
- [Releases](https://github.com/bitovi/react-to-web-component/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bitovi-react-to-web-component
