How to use Web Components with Next.js and TypeScript

swyx - Apr 3 '20 - - Dev Community

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

Normally to use a webcomponent in a project you just import it like a library:

  import 'wc-spinners/dist/atom-spinner.js';
Enter fullscreen mode Exit fullscreen mode

and then use it in our JSX/HTML:

<div> 
    <atom-spinner/> Loading
</div>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

The solution I got from this guy is to use declaration merging to extend TypeScript:

// declarations.d.ts - place it anywhere, this is an ambient module
declare namespace JSX {
  interface IntrinsicElements {
    "atom-spinner": any;
  }
}
Enter fullscreen mode Exit fullscreen mode

Next.js and Web Components

The next issue you run into is server side rendering.

ReferenceError: HTMLElement is not defined
Enter fullscreen mode Exit fullscreen mode

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>)
}
Enter fullscreen mode Exit fullscreen mode

And that's that! Enjoy using the platform!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .