- How to use Web Components with Next.js and TypeScript
- Web Components 101
- TypeScript and Web Components
- Next.js and Web Components
- How to use Web Components with TypeScript and React
- Web Component Essentials
- Web Component Essentials
- Consuming a Web Component in React in Typescript
- Option 1: Type Definitions in the web component
- Or use a tool to help
- Or.
- Option 2: Type Definition in the React app
How to use Web Components with Next.js and TypeScript
In my livestream today I had the need to bring in a spinner component to show work in progress in my app. However found that existing React spinners were too heavy. That’s when I had the idea to use web components in my Next.js (React/Preact) app for the first time ever! We’ll use https://github.com/craigjennings11/wc-spinners/ as a case study.
Web Components 101
import 'wc-spinners/dist/atom-spinner.js';
The web component will encapsulate behavior and styles without the weight of a framework, which is very nice for us. However when it comes to Next.js and TypeScript, we run into some problems.
TypeScript and Web Components
When you use TypeScript with JSX, it tries to check every element you use to guard against typos. This is a problem when you’ve just registered a component that of course doesn’t exist in the normal DOM:
Property 'atom-spinner' does not exist on type 'JSX.IntrinsicElements'.ts(2339)
// declarations.d.ts - place it anywhere, this is an ambient module declare namespace JSX interface IntrinsicElements "atom-spinner": any; > >
Next.js and Web Components
ReferenceError: HTMLElement is not defined
WC’s famously don’t have a great SSR story. If you need to SSR WC’s, it seems the common recommendation is to use a framework like Stencil.js. I have no experience with that. Since in my usecase I only needed the WC clientside, I found that I could simply defer loading the WC:
function Component() React.useEffect(() => import("wc-spinners/dist/atom-spinner.js") , []) return (div> // etc atom-spinner /> /div>) >
How to use Web Components with TypeScript and React
In our previous post, we learned how to use Web Components in React. React recently added experimental support of Web Components enabling our React apps to leverage the large Web Component ecosystem. We can also use Web Components in TSX/TypeScript templates providing type checking in our components.
We will use the same alert Web Component from the previous tutorial in our example.
To use the alert in our React component, we import the component and add the x-alert tag.
import React, useState > from 'react';
import './alert.js';
export default function App()
const [show, setShow] = useState(true);
return (
div> button onClick=() => setShow(!show)>>toggle alertbutton>
x-alert hidden=show> status="success" closable oncloseChange=() => setShow(!show)>>
This is a Web Component in React x-alert> div>
);
>
With the recent support for Custom Elements, we can now set properties and listen to events on our Web Component elements. If we are using TypeScript, however, we will likely see some errors in our TSX files like this below:
At runtime, React will work correctly with our Web Component. However, TypeScript cannot type check our TSX template due to it not knowing how to find the CustomElement class reference for our associated tag. We can add our tag to the JSX.IntrinsicElements global interface, which will associate x-alert to the XAlert class reference.
import React, useState, DOMAttributes > from 'react';
import XAlert > from './alert.js';
import './alert.js';
type CustomElementT> = PartialT & DOMAttributesT> & children: any >>;
declare global
namespace JSX
interface IntrinsicElements
['x-alert']: CustomElementXAlert>;
>
>
>
Using TypeScript Generics, we can pass and combine our Custom Element class XClass to the React DOMAttributes . This will add the React event types like onClick to our element. Now, if we look at our element, we should see the following,
Our component does not recognize the closeChange event that our Web Component emits when the user clicks the close button. However, we can leverage TypeScript Template Literal Types to define what events are available easily.
type CustomEventsK extends string> = [key in K] : (event: CustomEvent) => void >;
type CustomElementT, K extends string> = PartialT & DOMAttributesT> & children: any > & CustomEvents`on$K>`>>;
declare global
namespace JSX
interface IntrinsicElements
['x-alert']: CustomElementXAlert, 'closeChange' | 'openChange'>;
>
>
>
Using Template Literal Types, we can create a list of string types and generate a new list of string types with the on prefix required for React. Then we can take pass our event strings to the CustomEvents type, which will add the function signature for us. So now we have type checking on our custom events for our Web Components in React and TSX.
import React, useState, DOMAttributes > from 'react';
import XAlert > from './alert.js';
import './alert.js';
type CustomEventsK extends string> = [key in K] : (event: CustomEvent) => void >;
type CustomElementT, K extends string> = PartialT & DOMAttributesT> & children: any > & CustomEvents`on$K>`>>;
declare global
namespace JSX
interface IntrinsicElements
['x-alert']: CustomElementXAlert, 'closeChange' | 'openChange'>;
>
>
>
export default function App()
const [show, setShow] = useState(true);
return (
div> button onClick=() => setShow(!show)>>toggle alertbutton>
x-alert hidden=show> status='success'> closable oncloseChange=() => setShow(!show)>>
This is a Web Component in React x-alert> div>
);
>
With a bit of TypeScript Generics and Template Literal Types magic, we can create an excellent developer experience using TypeScript, React, and Web Components.
If you maintain a Web Component library check out my library custom-element-types which will generate type definitions for various frameworks as described in this blog post automatically. Otherwise check out the demo below!
Web Component Essentials
Save development time, improve product consistency and ship everywhere. With this new Course and E-Book learn how to build UI components that work in any JavaScript framework such as Angular, Vue, React, and more!
Web Component Essentials
Reusable UI Components for all your Web Applications
Consuming a Web Component in React in Typescript
After building my first web component I wanted to use it in my React app that was written in Typescript. I found some blog posts and tutorials about using web components in React apps, but none of those posts/tutorials were Typescript-based, so when I went to add my custom element I got the error [ts] Property ‘wc-menu-button’ does not exist on type ‘JSX.IntrinsicElements’. [2339] .
The problem is React only knows about standard HTML Elements, so when I go putting my custom element in there, it doesn’t know about it and shows me the error. Obviously Javascript React projects don’t have this error because there are no types (just mayhem and run-time bugs 🙃).
There are 2 ways to fix this. One way is in the web component, the other way is in the React app.
Option 1: Type Definitions in the web component
If you are the author of the web component you can declare your component as part of JSX’s IntrinsicElements, like this:
declare global
namespace JSX
interface IntrinsicElements
"my-element": any;
>
>
>
You might think that setting the type of my-element to any is less than ideal, and you’d be right. Wouldn’t it be better to define your attributes in an interface then set your custom element to that type? Something like this:
declare global
namespace JSX
interface IntrinsicElements
"my-element": MyElementAttributes;
>
interface MyElementAttributes
name: string;
>
>
>
This looks well and good, until you try to actually use that custom element in a React project and need to set the key or ref attributes. You’ll be met with an error like [ts] Property ‘ref’ does not exist on type ‘MyElementAttributes’. [2339] .
The MyElementAttributes needs to extend the React HTMLAttributes class, like this:
interface MyElementAttributes extends HTMLAttributes
name: string;
>
But if you try and compile your web component now then tsc (the Typescript compiler) will give you an error because it doesn’t know what the HTMLAttributes type is. You’ll need to take a dev dependency on React types ( @types/react ). But now your simple web component projects takes a dependency on React types, which might feel a little backward — your standards-based custom element doesn’t need to know about some front-end framework, right? Or maybe it’s fine with you. Either take that dev dependency on react, or you can just say your custom-element is of type any and make sure you have good documentation around the attributes (you should have good documentation anyway, actually).
Or use a tool to help
If you don’t want to deal with generating the IntrinsicElements types yourself, you can use a tool like StencilJS to build your web component. It will take care of types, including a global declare to define your web component inside the IntrinsicElements interface.
Caveat for Stencil — there might currently be a bug with Stencil type generation and React, so until that is fixed you can also put some extra info in your docs about how to handle type declarations in the React app
Or.
Make some good documentation and tell consumers of your web component to declare their own types, which leads us to.
Option 2: Type Definition in the React app
Similar concept to option 1, so I’ll keep this brief. With this options you make the type declaration file in the React app instead of in the web component. I usually just make a declarations.d.ts file and put the type declarations there:
declare namespace JSX
interface IntrinsicElements
"wc-menu-button": any;
>
>
Now my React app compiles and consumes my web component.
If you’d like to see a sample in action, I made a sample repo of a React app consuming a web component using option 2. Besides declaring types, this repo also demonstrates how to install, how to set attributes, and how to use ref to set properties of the web component.
I also made a repo in plain old Javascipt here.
Hopefully you found this post helpful, if you have any questions you can find me on Twitter.