A React component wrapper for web components that has graduated from labs and now serves as a proxy to @lit/react
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
React integration for Web Components and Reactive Controllers. This package has graduated from labs and now serves as a proxy that re-exports functionality from @lit/react while maintaining compatibility during the ecosystem migration.
npm install @lit-labs/reactimport { createComponent } from "@lit-labs/react";
import { useController } from "@lit-labs/react/use-controller.js";For CommonJS:
const { createComponent } = require("@lit-labs/react");
const { useController } = require("@lit-labs/react/use-controller.js");import * as React from 'react';
import { createComponent } from '@lit-labs/react';
import { MyElement } from './my-element.js';
// Create React component wrapper for custom element
export const MyElementComponent = createComponent({
tagName: 'my-element',
elementClass: MyElement,
react: React,
events: {
onactivate: 'activate',
onchange: 'change',
},
});
// Use the component in JSX
function App() {
const [isActive, setIsActive] = React.useState(false);
return (
<MyElementComponent
active={isActive}
onactivate={(e) => setIsActive(e.active)}
onchange={(e) => console.log('Changed:', e.detail)}
/>
);
}@lit-labs/react provides two main utilities:
@lit/react to maintain compatibility during migrationCreates React component wrappers for custom elements with proper property and event handling.
/**
* Creates a React component for a custom element
* @param options Configuration object for the wrapper component
* @returns React component that wraps the custom element
*/
function createComponent<I extends HTMLElement, E extends EventNames = {}>(
options: Options<I, E>
): ReactWebComponent<I, E>;
interface Options<I extends HTMLElement, E extends EventNames = {}> {
/** The React module, typically imported from the 'react' npm package */
react: typeof React;
/** The custom element tag name registered via customElements.define */
tagName: string;
/** The custom element class registered via customElements.define */
elementClass: Constructor<I>;
/** Object mapping React prop names to custom element event names */
events?: E;
/** React component display name, used in debugging messages */
displayName?: string;
}
type Constructor<T> = { new (): T };
type EventNames = Record<string, EventName | string>;Usage Examples:
import * as React from 'react';
import { createComponent } from '@lit-labs/react';
import { MyElement } from './my-element.js';
// Basic component creation
const MyElementComponent = createComponent({
tagName: 'my-element',
elementClass: MyElement,
react: React,
});
// Component with event mapping
const InteractiveComponent = createComponent({
tagName: 'interactive-element',
elementClass: InteractiveElement,
react: React,
events: {
onactivate: 'activate',
onchange: 'change',
onclick: 'click',
},
displayName: 'InteractiveElement',
});React hook for using Reactive Controllers within React components.
/**
* Creates and stores a stateful ReactiveController instance with React lifecycle
* @param React The React module that provides the base hooks (useState, useLayoutEffect)
* @param createController Function that creates a controller instance given a host
* @returns The controller instance
*/
function useController<C extends ReactiveController>(
React: typeof window.React,
createController: (host: ReactiveControllerHost) => C
): C;
type ControllerConstructor<C extends ReactiveController> = {
new (...args: Array<any>): C;
};Usage Examples:
import * as React from 'react';
import { useController } from '@lit-labs/react/use-controller.js';
import { MouseController } from '@example/mouse-controller';
// Create a React hook using a controller
const useMouse = () => {
const controller = useController(React, (host) => new MouseController(host));
return controller.position;
};
// Use the controller hook in a React component
const Component = () => {
const mousePosition = useMouse();
return (
<pre>
x: {mousePosition.x}
y: {mousePosition.y}
</pre>
);
};
// Direct controller usage
const MyComponent = () => {
const myController = useController(React, (host) => new MyCustomController(host));
return (
<div>
<p>Controller state: {myController.value}</p>
<button onClick={() => myController.increment()}>
Increment
</button>
</div>
);
};/**
* Type used to cast event names with specific event types for better typing
*/
type EventName<T extends Event = Event> = string & {
__eventType: T;
};
/**
* Type of the React component wrapping the web component
*/
type ReactWebComponent<
I extends HTMLElement,
E extends EventNames = {}
> = React.ForwardRefExoticComponent<
React.HTMLAttributes<I> & Partial<Omit<I, keyof HTMLElement>> & React.RefAttributes<I>
>;
/**
* Creates a type for props of a web component used directly in React JSX
*/
type WebComponentProps<I extends HTMLElement> = React.DetailedHTMLProps<
React.HTMLAttributes<I>,
I
>;
/**
* Type for constructor functions that create ReactiveController instances
*/
type ControllerConstructor<C extends ReactiveController> = {
new (...args: Array<any>): C;
};
/**
* External types from @lit/reactive-element (not part of @lit-labs/react API)
* These interfaces are imported when using useController
*/
interface ReactiveController {
hostConnected?(): void;
hostDisconnected?(): void;
hostUpdate?(): void;
hostUpdated?(): void;
}
interface ReactiveControllerHost {
addController(controller: ReactiveController): void;
removeController(controller: ReactiveController): void;
requestUpdate(): void;
readonly updateComplete: Promise<boolean>;
}import type { EventName } from '@lit-labs/react';
// Define typed events for better type safety
const MyElementComponent = createComponent({
tagName: 'my-element',
elementClass: MyElement,
react: React,
events: {
onClick: 'pointerdown' as EventName<PointerEvent>,
onChange: 'input' as EventName<InputEvent>,
},
});
// Now event callbacks are properly typed
<MyElementComponent
onClick={(e: PointerEvent) => console.log('Pointer event:', e)}
onChange={(e: InputEvent) => console.log('Input event:', e)}
/>declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'my-element': WebComponentProps<MyElement>;
}
}
}
// Now you can use the element directly in JSX
<my-element customProp={value} onCustomEvent={handler} />createComponent will warn if reserved React properties are found on the element prototypeuseController handles React Strict Mode correctly and manages controller lifecycle automaticallyComponents created with createComponent support server-side rendering when used with @lit/ssr-react. The wrapper handles:
defer-hydration attribute to prevent hydration warnings_$litProps$ for server rendering// SSR usage (works automatically with @lit/ssr-react)
const ServerRenderedComponent = createComponent({
tagName: 'my-element',
elementClass: MyElement,
react: React,
});
// Component will render properly on server and hydrate correctly on client
<ServerRenderedComponent customProp={value} />This package is deprecated and serves as a compatibility layer. For new projects, use @lit/react directly:
// Old (deprecated but still works)
import { createComponent } from '@lit-labs/react';
// New (recommended)
import { createComponent } from '@lit/react';