DEV Community

swyx
swyx

Posted on

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

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!

Top comments (2)

Collapse
 
noghte profile image
Saber Soleymani

Thank you. I was almost going crazy after 2 hours of trying! I thought the problem is one of the dependencies of the web component I tried to import.

Collapse
 
agpldev profile image
agpldev • Edited

@swyx a huge thank you for this article! I did go crazy trying to resolve this issue in my app for ages; the app was fine in npm run dev (with soft errors), but as soon as I came to npm run build the hard errors came up and I couldn't get it sorted.

Finally resolved by following your guidance here.