Wraps a React component as custom element for packaging and delivering.
Web Components is a modern suite of technology for reusable web UI components. It reduces the burden of web UI component developers to package and deliver their UI components to their customers.
However, custom element is primal. It requires a lot of attention on UI state transitioning, which React excels at.
By writing the UI component using React and delivering it as a custom element, we are enjoying the best of both worlds.
Assume the following React component.
type MyInputProps = { value?: string | undefined };
const MyInput = ({ value }: MyInputProps) => <input type="text" value={value} />;
To define the React component as a custom element, with data-value
attribute mapped to value
prop.
import { defineAsCustomElement } from 'react-define-as-custom-element';
defineAsCustomElement(MyInput, 'my-input', { 'data-value': 'value' });
Then, in the HTML, render it as:
<my-input data-value="Hello, World!"></my-input>
This would be equivalent to:
<MyInput value="Hello, World!" />
Rendering to shadow root is supported by passing shadowRoot
options, which is equivalent to the options passed to the HTMLElement.attachShadow()
function.
import { defineAsCustomElement } from 'react-define-as-custom-element';
defineAsCustomElement(MyInput, 'my-input', { 'data-value': 'value' }, { shadowRoot: { mode: 'open' } });
To customize built-in elements, use the builtInElement
options.
const MyButton = () => <span>Click me</span>;
defineAsCustomElement(
MyButton,
'my-button',
{},
{
builtInElement: {
customElementConstructor: HTMLButtonElement,
extends: 'button'
}
}
);
Then, in the HTML, use the is
attribute to specify the subclass custom element:
<button is="my-button"></button>
This approach should be rarely used. It assumes the host know the UI component is written in React, and vice versa. They should be integrated using React directly when possible.
Assume the React component is updated to receive a color
value from a React context:
const MyInput = ({ value }: { value?: string | undefined }) => {
const { color } = useContext(MyReactContext);
return <input type="text" style={{ color }} value={value} />;
};
To connect the underlying React component to a React context, use the defineAsCustomElementWithPortal
function:
import { defineAsCustomElementWithPortal } from 'react-define-as-custom-element';
const { Portal } = defineAsCustomElementWithPortal(MyInput, 'my-input', { 'data-value': 'value' });
render(
<MyReactContext.Provider value={{ color: 'Red' }}>
<Portal />
</MyReactContext.Provider>
);
The <Portal>
component must be mounted exactly once in your React app, where it will be receiving React context values from its ancestors.
Then, in the HTML:
<my-input data-value="Hello, World!"></my-input>
The underlying React component will be able to access the React context provided by <MyReactContext.Provider>
while rendering as a custom element. This is done by the React.createPortal()
function.
Call the useCustomElement
hook inside the converted React component to retrieve the instance of the custom element. This is useful when listening to events dispatched by slotted elements.
The following example dispatch an event from the custom element:
import { useCustomElement } from 'react-define-as-custom-element';
const MyInput = ({ value }: { value?: string | undefined }) => {
const [customElement] = useCustomElement();
const handleChange = useCallback(
({ currentTarget: { value } }) => customElement.dispatchEvent(new CustomEvent('input', { detail: { value } })),
[customElement]
);
return <input onChange={handleChange} type="text" value={value} />;
};
Custom method is not an escape hatch for passing non-string data to the underlying React component. Attribute is the best practice for passing data. It supports
<template>
and server-side rendering. In most cases, attributes should be used with serialization to pass non-string data.
Custom methods can be added to the custom element via the useMethodCallback()
hook.
import { defineAsCustomElement, useMethodCallback } from 'react-define-as-custom-element';
const MyCalculator = () => {
const [left, setLeft] = useState<number>(0);
const [right, setRight] = useState<number>(0);
const [sum, setSum] = useState<number>(left + right);
useMethodCallback('sum', (left: number, right: number): number => {
setLeft(left);
setRight(right);
setSum(left + right);
return left + right;
});
return (
<div>
{left} + {right} = {sum}
</div>
);
};
defineAsCustomElement('my-calculator', MyCalculator, {});
After calling the useMethodCallback('sum')
hook, a sum()
function will be added to the custom element.
document.getElementsByTagName('my-calculator')[0].sum(1, 2); // Returns 3.
Only one callback function can be registered per method name. The hook will throw if called with the method names that are already registered with another callback function.
There are some key differences between React component and a custom element:
- Attributes in custom elements are optional, must be of type string, and not limited to subset of strings
- Therefore, props in React component must be optional strings too and expect caller to pass unsupported or invalid values
- When using shadow DOM, the slotted element (denoted by the
slot
attribute) is virtually relocated (a.k.a. shadowed) to the<slot>
placeholder element- The parent of the slotted element is root of the custom element, not the
<slot>
element - Events propagated from the slotted element will not available to the
<slot>
element but only to the root of the custom element
- The parent of the slotted element is root of the custom element, not the
- When updating multiple attributes/props synchronously
- For custom element, browser will call
HTMLElement.attributeChangeCallback()
multiple times, therefore, component must support eventual consistency - For React component, all props changes will be updated at once
-
defineAsCustomElement()
always batch multiple synchronous attribute changes into a single React render call usingqueueMicrotask
- For custom element, browser will call
- Custom elements must not be self-closing
- The name of custom elements must follow requirements defined in the HTML Living Standard
Consider these factors: cost of writing a new UI component, making them on parity of each other, and maintaining both of them.
Writes an adapter component that parses the string into a number.
No, custom elements is a HTML technology and is only available in modern browsers.
Adjusts and adds the following declaration to your project.
declare global {
namespace JSX {
interface IntrinsicElements {
'my-input': HTMLAttributes<HTMLElement> & { value?: string | undefined };
}
}
}
To support React version 16.8 to 18, we are using ReactDOM.render
, which is available through out the supported versions.
After defined the custom element, call customElements.get()
to retrieve the custom element constructor and modify as needed.
- API to support
formAssociated
, HTML Constraint Validation and Accessibility Object Model - API to support
CustomStateSet
Like us? Star us.
Want to make it better? File us an issue.
Don't like something you see? Submit a pull request.