ce-la-react
ce-la-react is a utility library designed to simplify the integration of vanilla custom elements with React components, providing a wrapper that addresses common issues with prop passing and event handling. The current stable version is 0.3.2. It maintains a fairly active release cadence with frequent patch releases, indicating ongoing development. A key differentiator from similar libraries like `@lit/react` is its stronger emphasis on server-side rendering (SSR) by favoring attributes over properties for primitive values. It also offers configurable conversion functions for properties to attributes and supports custom element templates via a static `getTemplateHTML` method, offering a robust solution for developers building component libraries with Web Components that need to be consumed by React applications.
Common errors
-
ReferenceError: HTMLElement is not defined
cause This error typically occurs during server-side rendering (SSR) if the Node.js environment does not have a global `HTMLElement` available, which is a browser-specific API.fixEnsure that your SSR setup either mocks `HTMLElement` (and other DOM APIs) or that the custom element definition is guarded to only run in a browser environment, or use a tool that provides a browser-like environment for SSR. -
TypeError: Cannot set properties of undefined (setting 'active') or elementClass.prototype undefined
cause This specific error ('elementClass.prototype undefined error with corejs polyfill') was a known bug related to interactions with `core-js` polyfills, especially in older versions.fixUpgrade to `ce-la-react` version 0.3.1 or newer. If the issue persists, review your `core-js` polyfill configuration to ensure it's compatible with modern browser APIs and custom element specifications. -
Type 'CustomEvent<MyDetail>' is not assignable to type 'Event'
cause This TypeScript error occurs when a custom event callback expects a specific `CustomEvent` type (e.g., `CustomEvent<MyDetail>`) but the `events` map in `createComponent` was not correctly type-casted with `EventName<CustomEvent<MyDetail>>`, causing the callback parameter to default to the generic `Event` type.fixIn your `createComponent` configuration, explicitly type cast your custom event names. For example, `events: { onMyEvent: 'my-event' as EventName<CustomEvent<MyDetail>> }`.
Warnings
- gotcha When defining event callbacks in TypeScript, it is crucial to explicitly cast the event name using `EventName<YourCustomEventType>` (e.g., `onToggle: 'toggle' as EventName<CustomEvent<ToggleEventDetail>>`). Failing to do so will cause the event parameter in the React callback to default to a generic `Event` type, leading to potential type errors or missing properties.
- breaking While `ce-la-react` aims for improved React 19+ compatibility, earlier versions (specifically `<0.2.1`) had a bug related to 'complex reflected prop/attr' handling with React 19+. Users experiencing issues with complex properties or attributes might be on an outdated version.
- gotcha `ce-la-react` prioritizes attributes over properties for primitive values to enhance Server-Side Rendering (SSR) compatibility. Developers accustomed to directly setting properties on custom elements in React might need to adjust their mental model, particularly for primitive data types.
Install
-
npm install ce-la-react -
yarn add ce-la-react -
pnpm add ce-la-react
Imports
- createComponent
const { createComponent } = require('ce-la-react');import { createComponent } from 'ce-la-react'; - EventName
import type { EventName } from 'ce-la-react'; - React
import React from 'react';
import * as React from 'react';
Quickstart
import * as React from 'react';
import { createComponent } from 'ce-la-react';
import type { EventName } from 'ce-la-react';
// Define a simple custom element for demonstration purposes
class MyToggleElement extends HTMLElement {
static observedAttributes = ['active'];
private _active: boolean = false;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot!.innerHTML = `<style>
:host { display: inline-block; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
button { padding: 8px 12px; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 3px; }
button.active { background: #28a745; }
</style>
<button>${this._active ? 'Active' : 'Inactive'}</button>`;
this.shadowRoot!.querySelector('button')!.onclick = () => {
this.active = !this.active;
};
}
set active(value: boolean) {
if (this._active === value) return;
this._active = value;
if (value) {
this.setAttribute('active', '');
} else {
this.removeAttribute('active');
}
this.updateButtonText();
this.dispatchEvent(new CustomEvent('toggle', { detail: { active: value }, bubbles: true, composed: true }));
}
get active(): boolean {
return this._active;
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
if (name === 'active') {
this.active = newValue !== null;
}
}
private updateButtonText() {
const button = this.shadowRoot!.querySelector('button');
if (button) {
button.textContent = this._active ? 'Active' : 'Inactive';
button.classList.toggle('active', this._active);
}
}
}
customElements.define('my-toggle-element', MyToggleElement);
interface ToggleEventDetail { active: boolean; }
// Create the React component wrapper
export const MyToggleComponent = createComponent({
tagName: 'my-toggle-element',
elementClass: MyToggleElement,
react: React,
events: {
onToggle: 'toggle' as EventName<CustomEvent<ToggleEventDetail>>
},
});
// Example React usage
export function App() {
const [isActive, setIsActive] = React.useState(false);
return (
<div>
<h1>ce-la-react Custom Element Demo</h1>
<p>React State: {isActive ? 'ON' : 'OFF'}</p>
<MyToggleComponent
active={isActive}
onToggle={(e: CustomEvent<ToggleEventDetail>) => {
console.log('Custom event received:', e.detail.active);
setIsActive(e.detail.active);
}}
/>
<p>Click the custom element button to toggle its state and see React update.</p>
</div>
);
}
// To run this in a real app, you would typically render `App`:
// import { createRoot } from 'react-dom/client';
// const container = document.getElementById('root');
// if (container) {
// const root = createRoot(container);
// root.render(<App />);
// }