🙆

Next.jsでLitで作成したWeb Componentsを使うのは苦労する

2023/03/10に公開

React(Next.js)でWeb Componentsを使用するのが難しい理由

データの扱い方

Reactは、カスタム要素にデータをHTML属性の形式で渡します。単純なデータの場合は問題ありませんが、オブジェクトや配列などの複雑なデータを渡すと、[object Object]のような文字列化された値が渡されてしまい、実際には使用できません。

イベントの扱い方

Reactは独自のSynthetic Eventシステムを実装しているため、カスタム要素から発生したDOMイベントを直接リッスンすることができません。そのため、カスタム要素をrefで参照し、addEventListenerで手動でイベントリスナーを設定する必要があります。

Custom Elements Everywhere

@lit-labs/reactを使用する

この問題を解決する方法として、@lit-labs/reactを使用してカスタム要素のReactコンポーネントラッパーを作成します。これにより、Reactのpropsをカスタム要素が受け入れるプロパティに正しく渡すことができ、カスタム要素からディスパッチされるイベントをリッスンすることができます。

ただし、NextjsでLitを使用する場合、クライアントサイドのレンダリングだけが必要な場合でも、ReferenceError: window is not defined のエラーが発生します。

Litを読み込むためには、@lit-labs/ssrパッケージで提供されるdom-shim.jsモジュールを読み込む必要があります。このdom-shim.jsモジュールには、windowやHTMLElement、customElementsなどのAPIが定義されているためです。

[ssr] Can't bundle into SSR app because of side-effects

ダイナミックインポートを使用する

Next.jsでは、コンポーネントをクライアント側で動的にロードするために、ssrオプションを使ってサーバーレンダリングを無効化することができます。ただし、この方法では、TypeScriptのエラーが発生してうまく動作しません。

const Input = dynamic(
  () => import('@org/design-system').then(({ Input }) => Input),
  {
    ssr: false,
  },
);
Argument of type '() => Promise<typeof Input>' is not assignable to parameter of type 'DynamicOptions<{}> | Loader<{}>'.
  Type '() => Promise<typeof Input>' is not assignable to type '() => LoaderComponent<{}>'.
    Type 'Promise<typeof Input>' is not assignable to type 'LoaderComponent<{}>'.
      Type 'typeof Input' is not assignable to type 'ComponentType<{}> | { default: ComponentType<{}>; }'.
        Type 'typeof Input' is not assignable to type 'ComponentClass<{}, any>'.
          Type 'Input' is missing the following properties from type 'Component<{}, any, any>': context, setState, forceUpdate, props, and 2 more.

[labs/react] Issues using lit-labs wrappers in Next.js

解決策

@org/design-systemをLitで作成したコンポーネントライブラリだと仮定すると、このライブラリを@lit-labs/reactでラップする@org/design-system-reactのようなライブラリを作成します。

@org/design-system

import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
  // Define scoped styles right with your component, in plain CSS
  static styles = css`
    :host {
      color: blue;
    }
  `;

  // Declare reactive properties
  @property()
  name?: string = 'World';

  // Render the UI as a function of component state
  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}

@org/design-system-react

import { SimpleGreeting } from '@org/design-system';
import { createComponent } from '@lit-labs/react';
import * as React from 'react';

export const SimpleGreetingComponent = createComponent({
  tagName: 'simple-greeting',
  elementClass: SimpleGreeting,
  react: React,
});

これをNext.jsでインポートします。

import type { SimpleGreetingComponent as SimpleGreetingComponentType } from '@org/design-system-react';
import dynamic from 'next/dynamic';

const SimpleGreetingComponentType = dynamic<React.ComponentProps<typeof SimpleGreetingComponentType>>(
  () => import('@org/design-system-react').then(({ SimpleGreetingComponent }) => SimpleGreetingComponent),
  {
    ssr: false,
  },
);

Discussion