Wrapping Lit React Wrappers
Recently I had been working through some tree-shaking issues in Shoelace
To effectively tree-shake and open up the possibility of Scoped Custom Elements
Now I thought this would be easy, but turns out I didn’t want to add JSX transpiling to our bundle.
I’ll save you the steps I went through and give you the final product of how to wrap a
React Wrapper from @lit-labs/react
.
import * as React from 'react';
import { createComponent } from '@lit-labs/react';
import SlButton from '@shoelace-style/shoelace/dist/components/button/button.component.js';
const tagName = "sl-button"
const component = createComponent({
tagName,
elementClass: SlButton,
react: React,
events: {},
displayName: "SlButton"
})
class SlButtonComponent extends React.Component<Parameters<typeof component>[0]> {
constructor (...args: Parameters<typeof component>) {
super(...args)
// Register the customElement. Technically we could do this on `componentDidMount()` as well.
if (!customElements.get(tagName)) {
customElements.define(tagName, SlButton)
}
}
render () {
const { children, ...props } = this.props
return React.createElement(component, props, children)
}
}
export default SlButtonComponent;
Now technically createComponent
could be considered a side-effect. I haven’t looked
at the Lit implementation. To get TS types right it takes a little bit more work, but we could also do
something like this where we dont define the component until the constructor has been initialized.
import * as React from 'react';
import { createComponent } from '@lit-labs/react';
import SlButton from '@shoelace-style/shoelace/dist/components/button/button.js';
const tagName = "sl-button"
const events = {
onSlChange: "sl-change"
} as const
type SlButtonComponentType = ReturnType<typeof createComponent<SlButton, typeof events>>
type Props = Parameters<SlButtonComponentType>[0]
class SlButtonComponent extends React.Component<Props> {
// Make it static so we only evaluate this once instead of for every instance.
static component: SlButtonComponentType = createComponent({
tagName,
elementClass: SlButton,
react: React,
events,
displayName: "SlButton"
})
constructor (...args: ConstructorParameters<typeof React.Component<Props>>) {
super(...args)
// Register the customElement. Technically we could do this on `componentDidMount()` as well.
if (!customElements.get(tagName)) {
customElements.define(tagName, SlButton)
}
}
render () {
const { children, ...props } = this.props
return React.createElement(SlButtonComponent.component, props, children)
}
}
export default SlButtonComponent;
As you can see using a component that lives within the class requires a bit more work to get the typings right. The beauty of both examples above is we didn’t need JSX!
React.createElement
was all we needed! The class based components are what I showed here, but
you could also use hooks if you’re more familiar!
Here’s how we could do it using hooks and functional components:
import * as React from 'react';
import { createComponent } from '@lit-labs/react';
import SlButton from '@shoelace-style/shoelace/dist/components/button/button.component.js';
const tagName = "sl-button"
const component = createComponent({
tagName,
elementClass: SlButton,
react: React,
events: {},
displayName: "SlButton"
})
function SlButtonComponent (props: Paramaters<typeof component>[0]) {
React.useEffect(() => {
// Register the customElement on initial mount.
if (!customElements.get(tagName)) {
customElements.define(tagName, SlButton)
}
}, [])
const { children, ...props } = props
return React.createElement(component, props, children)
}
export default SlButtonComponent;
Conclusion
And that ends this fun exploration into how we can take a React component created by Lit and extend it to create our own component to do additional work on lifecycles.
I used this in Shoelace
Here the PR if you’re interested. (As of this blog post still WIP)