1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

「haunted」 - React hooks APIのWeb Component実装

Posted at

hauntedとは

最近何かと話題に上る「React hooks」ですが、React以外でも「hook」したいですよね。

hauntedは、標準のWeb Componentで「hook」する為のライブラリです。

ライセンスは「BSD-2-Clause」です。1

読みは、「ホーンテッド」?かと。浦安のハツカネズミの生息地にそんなマンションがあったような

執筆時点で、バージョンは「4.5.4」です。

使い方

hauntedはデフォルトでlit-htmlを使う前提になっています。

以下は、githubにあるコードです。

〇〇.htmlで保存し、ブラウザで開けば動きます。(僕の場合、Goole Chrome 76.0.3809.132)

カスタムエレメントを定義する場合の例です。

<!doctype html>
<html lang="en">

<my-counter></my-counter>

<script type="module">
  import { html } from 'https://unpkg.com/lit-html/lit-html.js';
  import { component, useState } from 'https://unpkg.com/haunted/haunted.js';

  function Counter() {
    const [count, setCount] = useState(0);

    return html`
      <div id="count">${count}</div>
      <button type="button" @click=${() => setCount(count + 1)}>Increment</button>
    `;
  }

  customElements.define('my-counter', component(Counter));
</script>

Counter関数は、lit-htmlのTemplateResultを作る関数です。

component関数は、lit-htmlのrenderTemplateResultを渡し、HTMLElementのクラスを返します。

つまり、

component関数 = Web Componentにおける「コンポーネント = HTMLElementを作る関数」

と捉えておいてください。

そのままカスタムエレメントとして登録できるということです。

別のDOM生成ライブラリも使える

「haunted」の目玉機能として、adoptableなところがあります。

lit-html以外のhyperhtmlなどのDOM生成ライブラリも使える仕組みです。

Importing

しかしせっかくなので、「hauntedのadoptableなところ」をお見せする為に、素のDOM APIを使ってみます。

先ほどのサンプルを少し書き換えました。

<!DOCTYPE html>
<html lang="en">
  <my-counter></my-counter>

  <script type="module">
    import { html } from "https://unpkg.com/lit-html/lit-html.js";
    import haunted, { useState } from "https://unpkg.com/haunted/haunted.js";

    const { component } = haunted({
      // DOM総入れ替えの為、とてつもなく効率が悪い
      render(what, where) {
        // 親要素の中身をすべてクリア
        while (where.hasChildNodes()) {
          where.removeChild(where.firstChild);
        }

        // Counter関数で作ったDOM要素をぶち込む
        what.forEach(element => {
          where.appendChild(element);
        });
      }
    });

    function Counter() {
      const [count, setCount] = useState(0);

      // カウントする数字を見せる枠
      const countContainer = document.createElement("div");
      const countText = document.createTextNode(count);
      countContainer.appendChild(countText);

      // カウントアップのボタン
      const countupButton = document.createElement("button");
      countupButton.innerText = "Increment";
      countupButton.setAttribute("type", "button");
      countupButton.addEventListener("click", e => {
        setCount(count + 1);
      });

      return [countContainer, countupButton];
    }

    customElements.define("my-counter", component(Counter));
  </script>
</html>

今回作ったCounter関数は、「DOM要素の配列」を返します。

haunted関数に渡すrender関数には、「whatwhere」が引数として渡されますが、

whatには、Counter関数の戻り値 = 「DOM要素の配列」が入っています。

whereは、親要素が渡されます。

今回は、親要素を空にして、DOM要素の配列を順番に入れていくだけの非効率ロジックですが、説明の為なのでご勘弁を。

属性を渡すには

作ったコンポーネントに属性を渡すには、observedAttributesを使います。

下は、value属性を渡して、初期値にする場合の例です。

<!DOCTYPE html>
<html lang="en">
  <my-counter value="100"></my-counter>

  <script type="module">
    import { html } from "https://unpkg.com/lit-html/lit-html.js";
    import { component, useState } from "https://unpkg.com/haunted/haunted.js";

    function Counter({ value }) {
      const [count, setCount] = useState(Number(value) || 0);

      return html`
        <div id="count">${count}</div>
        <button type="button" @click=${() => setCount(count + 1)}>
          Increment
        </button>
      `;
    }

    Counter.observedAttributes = ["value"];

    customElements.define("my-counter", component(Counter));
  </script>
</html>

属性には文字列かundefinedが来ると思うので、その前提でハンドリングするといいと思います。

今回の例は、Number()に渡して、NaNだったら強制的に0にしています。

属性で文字列しか渡せないのは嫌だ

「属性として」じゃなくて、「JavaScript関数の引数」として値を渡したいですよね。

コンポーネントを関数として、配列とかオブジェクトを引数として渡せるようにしたいです。

そんなときは、virtual関数を使いましょう。

<!DOCTYPE html>
<html lang="en">
  <my-app></my-app>

  <script type="module">
    import { html } from "https://unpkg.com/lit-html/lit-html.js";
    import {
      component,
      useState,
      virtual
    } from "https://unpkg.com/haunted/haunted.js";

    function Counter({ value }) {
      const [count, setCount] = useState(value);

      return html`
        <div id="count">${count}</div>
        <button type="button" @click=${() => setCount(count + 1)}>
          Increment
        </button>
      `;
    }

    const VirtualCounter = virtual(Counter);

    function App() {
      return html`
        ${VirtualCounter({ value: 100 })}
      `;
    }

    customElements.define("my-app", component(App));
  </script>
</html>

さっきと変わったのは、Counter関数をvirtual関数でラップしたVirtualCounterを作っている点です。

VirtualCounter({ value: 100 })というように、普通に関数として呼び出せてます。

あとは、<my-app>コンポーネントを定義して、描写しているだけです。簡単。

しかしこちらは、lit-htmlのdirectiveを使っているようなので、別のDOM生成ライブラリの利用はできないようです。

「hook」を自作する

もちろん「hook」を自作できます。

Write Your Own Hook

useMyUseStateという「hook」を作ってみました。

本家のuseStateの1000倍くらい遅くて、危なっかしい俺のuseStateを使えというオラオラhookです。

    import {
      hook,
      Hook
    } from "https://unpkg.com/haunted/haunted.js";

    const useMyUseState = hook(
      class extends Hook {
        constructor(id, el, value) {
          super(id, el);

          this.value = value;
        }

        update(value) {
          return [
            this.value,
            value => {
              this.value = value;
              this.el.update();
            }
          ];
        }

        teardown() {}
      }
    );

updateメソッドは、setStateが呼び出される毎に呼ばれます。

useMyUseStateでは、そのたびにthis.valueを更新し、this.el.update()でコンポーネントを再度描写しています。

teardownは、

  • カスタムエレメントだとdisconnectedCallback

  • virtual componentだと、「DOMに変更があったとき、親ノードがShadowRootかどうかで判定している」のでたぶんコンポーネントが消えた時でいいと思います。

まとめ

正直、lit-htmlを昔使ったとき不満が多くてやめたんですが、virtual componentとか、lit-html使ってみたくなる機能が入ってて、universal-routerとか組み合わせて、アプリ作ってみたら楽しそうだなあと思いました。

あと、ドキュメント読んで分からないところとかはソースコードを読んだので、もしかしたら間違ってるかもしれません。

その時はご指摘いただけるとありがたいです。

正直、わかりやすい記事書けた感が無いので、もっと文才のある方の記事をお待ちしております。(投げやり)

そんなこんなで、hountedの紹介でした。

  1. 2019/09/19 現在

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?