🐜

Swiper v10からReact Componentsが非推奨になったのでWebコンポーネントを使う

2024/02/16に公開

Swiperのバージョンがv9からv10になって、Reactコンポーネントが非推奨になってしまったようです。

現在v11がリリースされており、バージョンアップの際は「Webコンポーネントを使うように」と説明がありました。

https://swiperjs.com/react

公式ドキュメントどおりに進めたが、いくつかハマる箇所がありました。
今回その対応方法について説明します。

対象読者

  • ReactでSwiperのv10以降に移行したい人
  • Reactでカスタムコンポーネントを使いたい人
  • Webコンポーネントがわからない人

説明すること/説明しないこと

  • 説明すること

    • Webコンポーネントってなんだっけ?
    • 公式どおりやっても動かないし、型エラーになる
    • うまくいった方法
  • 説明しないこと

    • React.jsの基本的な処理

いいね!してね

この記事の事例は必要に応じて今後追記していく予定です!
「新しい事例が知りたい」「他の事例も知りたい」と思った人は、ぜひこの記事にいいね👍してください。筆者のモチベーションにつながります!

それでは以下が本編です。

結論

  • Webコンポーネントとは、HTMLの仕様に基づいたタグ(例: titleタグ、bodyタグ、liタグ)を指す
  • React内でカスタムコンポーネント(例: <swiper-container/>や<swiper-slide />)を使いたい場合は、その型を定義する
  • 動作確認したソースコードはこちら

Webコンポーネントってなんだっけ?

Q. Webコンポーネント という言葉の定義を教えてください

chatGPT

Webコンポーネントは、ウェブ開発における再利用可能な部品や要素を指す用語です。これは、特定の機能や外観を持つ単一の要素であり、独自のHTML、CSS、JavaScriptコードを持ち、他の要素と独立して動作します。

Webコンポーネントは、一般的にカスタム要素として実装され、HTMLの拡張機能を使用して定義されます。これにより、開発者は独自のカスタム要素を定義し、それを再利用することができます。Webコンポーネントは、一貫したデザインや機能を保証するために広く使用され、コードの保守性と再利用性を向上させるのに役立ちます。

Webコンポーネントは通常、次の3つの主要な機能を持ちます:

  1. Shadow DOM(シャドウDOM):コンポーネントのスタイルや構造を他の要素と分離するための仕組み。これにより、コンポーネント内のスタイルが他の要素に影響を与えることがなくなります。

  2. HTMLテンプレート:コンポーネントのマークアップ構造を定義するためのテンプレート。これにより、コンポーネントの外観を簡単に再利用できます。

  3. Custom Elements(カスタム要素):独自のHTML要素を定義するためのAPI。これにより、開発者は独自のカスタム要素を作成し、それをHTML内で使用することができます。

Webコンポーネントの導入により、ウェブ開発者はコードをより構造化し、保守しやすく、効率的に再利用できるようになりました。

Q HTMLテンプレートとCustom Elementsを対比して説明してください。CustomElementsはReact.js + typescriptを使った場合の例で説明してください

chatGPT

HTMLテンプレートとCustom Elementsは、共にWebコンポーネントの開発に使用される機能ですが、異なる役割を果たします。以下ではそれぞれの機能をReact.jsとTypeScriptを使用した例を交えて説明します。

  1. HTMLテンプレート:

    • 役割: HTMLテンプレートは、コンポーネントのマークアップ構造を定義するためのテンプレートシステムです。これにより、コンポーネントの外観を定義し、再利用性を高めることができます。
    • : HTMLテンプレートを使用する場合、次のように記述できます。
    <template id="custom-template">
        <div>
            <p>Hello, <span id="name"></span>!</p>
        </div>
    </template>
    
  2. Custom Elements:

    • 役割: Custom Elementsは、独自のHTML要素を定義するためのAPIです。これにより、開発者は独自の要素を作成し、それをHTML内で使用できます。
    • : React.js + TypeScriptを使用してCustom Elementsを作成する場合、次のように記述できます。
    class CustomGreeting extends HTMLElement {
        constructor() {
            super();
    
            // Shadow DOMを設定
            const shadow = this.attachShadow({ mode: 'open' });
    
            // テンプレートの取得
            const template = document.getElementById('custom-template') as HTMLTemplateElement;
    
            // テンプレートの内容をコピーしてShadow DOMに追加
            const instance = template.content.cloneNode(true);
            shadow.appendChild(instance);
    
            // 要素に属性を設定
            const name = this.getAttribute('name');
            shadow.getElementById('name').textContent = name;
        }
    }
    
    // カスタム要素として登録
    customElements.define('custom-greeting', CustomGreeting);
    

この例では、Custom Elementsを使用して<custom-greeting>という独自の要素を定義しています。この要素は、nameという属性を取り、その値をテンプレート内の要素に埋め込んでいます。また、Shadow DOMを使用してコンポーネントのスタイルと構造をカプセル化しています。

このように、HTMLテンプレートはコンポーネントのマークアップを定義し、Custom Elementsはそれらのマークアップを操作し、独自の要素を定義する役割を果たします。

公式ドキュメントどおりやっても動作しない

Usage with React

https://swiperjs.com/element#usage-with-react
-> 公式ドキュメントがあったのでそのままコピペしてみたが、動作しない + 型エラーになる。

Custom Wrapper Component

https://swiperjs.com/blog/using-swiper-element-in-react#custom-wrapper-component

下記のコードは、あえて書かなくても動作するはず、、。
特に下記の部分。これをもし書いてしまうと無駄なコードが増えてしまうので避けたい。

  const swiperRef = useRef(null);
  const {
    children,
    ...rest
  } = props;

  useEffect(() => {
    // Register Swiper web component
    register();

    // pass component props to parameters
    const params = {
      ...rest
    };

    // Assign it to swiper element
    Object.assign(swiperRef.current, params);

    // initialize swiper
    swiperRef.current.initialize();
  }, []);

うまくいった方法

下記の方法で、SwiperのWebコンポーネント ⇔ Reactのカスタムコンポーネントをうまく連携させることができた。

コンポーネント

カスタムコンポーネントを呼ぶだけで済んだので、期待どおりスッキリした形に収まった!

import { useEffect, useRef } from "react";
import { register, SwiperContainer } from "swiper/element/bundle";
import "swiper/css";
import "swiper/css/navigation";
import Swiper from "swiper/types/swiper-class";

register();

export default function SwiperComponent() {
  const swiperElRef = useRef<SwiperContainer>(null);

  useEffect(() => {
    function onSwiperSlideChange(event: CustomEvent<[e: Swiper]>) {
      console.log("realIndex", event.detail[0].realIndex);
    }

    const ref = swiperElRef?.current;
    // swiper@v11.xから 「prefix `swiper` + swiper-element.d.ts内のcustomEvent名」を結合させたイベント名を指定する必要がある
    // @ts-ignore
    ref?.addEventListener("swiperslidechange", onSwiperSlideChange);
    return () => {
      // @ts-ignore
      ref?.removeEventListener("swiperslidechange", onSwiperSlideChange);
    };
  }, [swiperElRef]);

  return (
    <div>
      <swiper-container
        ref={swiperElRef}
        slides-per-view="2"
        autoplay-delay="4000"
        autoplay-disable-on-interaction="false"
        initial-slide="0"
        centered-slides="true"
        navigation-prev-el=".button-prev" // assign navigation class
        navigation-next-el=".button-next" // assign navigation class
        speed="500"
      >
        <swiper-slide key="1">1</swiper-slide>
        <swiper-slide key="2">2</swiper-slide>
      </swiper-container>

      {/* navigation */}
      <div className="button-prev absolute top-[3px] z-10 cursor-pointer flex justify-center left-2">
        {"<"}
      </div>
      <div className="button-next absolute top-[3px] z-10 cursor-pointer flex justify-center right-2">
        {">"}
      </div>
    </div>
  );
}

型エラーの解消

カスタムコンポーネント(ケバブケース)の型定義方法:https://github.com/nolimits4web/swiper/issues/6466
を参考にして、下記のような型定義を追加したら解消しました。

import Swiper, { SwiperOptions } from "swiper";
import { ReactNode, RefObject } from "react";

type Kebab<
  T extends string,
  A extends string = ""
> = T extends `${infer F}${infer R}`
  ? Kebab<R, `${A}${F extends Lowercase<F> ? "" : "-"}${Lowercase<F>}`>
  : A;

/**
 * Helper for converting object keys to kebab case because Swiper web components parameters are available as kebab-case attributes.
 * @link https://swiperjs.com/element#parameters-as-attributes
 */
type KebabObjectKeys<T> = {
  [key in keyof T as Kebab<key & string>]: T[key] extends Object
    ? KebabObjectKeys<T[key]>
    : T[key];
};

/**
 * Swiper 9 doesn't support Typescript yet, we are watching the following issue:
 * @link https://github.com/nolimits4web/swiper/issues/6466
 *
 * All parameters can be found on the following page:
 * @link https://swiperjs.com/swiper-api#parameters
 */
type SwiperRef = HTMLElement & { swiper: Swiper; initialize: () => void };

declare global {
  namespace JSX {
    interface SwiperContainerAttributes extends KebabObjectKeys<SwiperOptions> {
      // 指定したい値を適宜追加する
      ref?: RefObject<SwiperRef>;
      children?: ReactNode;
      speed?: string;
      class?: string;
    }
    interface SwiperSlideAttributes {
      // 指定したい値を適宜追加する
      children?: ReactNode;
      key?: string;
      class?: string;
    }

    interface IntrinsicElements {
      "swiper-container": SwiperContainerAttributes;
      "swiper-slide": SwiperSlideAttributes;
    }
  }
}

まとめ

「Webコンポーネント」の意味がよくわからず、困った。

オープンソースのライブラリを使うと、こういうバージョンアップに振り回される。
使用するかどうかは慎重に判断したい。

Discussion