🍁

SvelteのLifecycleメソッドとActions機能を改めて確認する

2021/12/22に公開

はじめに

この記事は Svelte Advent Calendar 2021 の 22 日目の記事です。筆者にとって本アドベントカレンダーでは 2 つ目の記事となります。

最近 Svelte の Contributor の 1 人である Geoff Rich 氏が以下の 2 記事を投稿していました。

https://blog.logrocket.com/svelte-actions-introduction/
https://geoffrich.net/posts/svelte-lifecycle-examples/

筆者は React(hooks ベース)を書くことも多いのですが、React のコードから Svelte に置き換える際、少しでも hooks 特有の書き方が載っていると書き方の違いで結構戸惑うことが多く困っていました(あれ?React だとすんなり書けるけど、Svelte だとこの時どうしたらいいのだろう?みたいな気持ちです。)。

そんな感じでモヤモヤしたまま日々を過ごしてたのですが、そんなとき上記記事が投稿されていたので読んでみたところ、Svelte の基礎がかなり抜けていることに気付かされました。

今回は上記記事を読んで学んだことを基に、Svelte の LifecycleActions について改めて復習していこう的な記事になります。元記事はすべて生の JavaScript で書かれていますが、可能な限り TypeScript(以下 ts)に置き換えています。

開発環境

PC: MacBook Pro(Apple M1, 2020)
OS: macOS Monterey 12.0.1

サンプルコード

https://github.com/miily8310s/svelte-review-article

REPL について

まず Svelte コードを書く上で、便利なツールを紹介しておきます。
REPL は Svelte の公式 PlayGround になります。React や Vue の公式にはこの手の機能はないので、Svelte の魅力の 1 つだと個人的には思っています。またアカウント登録しておくことで、コードの共有も出来ます。

チュートリアルもこの REPL を使って、進める方式になっています。ブラウザ上で色々試しながら勉強できるのは、かなりいいですよね。

https://svelte.dev/repl/

Lifecycle

Svelte には Lifecycle を司る関数として、onMount/onDestroy/beforeUpdate/afterUpdate/tick の 5 つがあります。今回は、Geoff 氏の元ネタ記事に従い、tick 以外の関数を紹介します。

onMount

https://svelte.dev/tutorial/onmount

https://svelte.dev/docs#run-time-svelte-onmount

onMount は、component の mount が初期化された際に走らせる callback を返します。Vue <= 2 の created や、React の componentDidMount(hooks 全盛になった今も一応健在)の立ち位置。書き方は以下の通りとなります。

<script lang="ts">
  import { onMount } from "svelte";

  onMount(() => {
    console.log("コンポーネントが初めてマウントされました");
  });
</script>

return を返す場合、return でラップした処理は component が DOM から破棄した場合に走ります。この点は注意が必要です。

<script lang="ts">
  import { onMount } from "svelte";

  onMount(() => {
    const interval = setInterval(() => {
      console.log("こーる");
    }, 1000);

    return () => clearInterval(interval);
  });
</script>

onMount がすごいところは、.svelte ファイル内で書く必要はなく、ts ファイルに別途切り出すことができる点です(これは後述する onDestroy/beforeUpdate/afterUpdate に関しても同様)。

JSONPlaceholder から fetch したデータを store に登録し、画面に mount してみます。

fetchTodos.ts
import { onMount } from "svelte";
import { todos } from "../store";

export const fetchTodos = () => {
  onMount(async () => {
    const res = await fetch(
      "https://jsonplaceholder.typicode.com/todos?_limit=10"
    );
    todos.set(await res.json());
  });
};

そしてこの store でコンポーネントを表示してみます。Svelte では store は $ をつけて取得することが出来ます。

MountWithFetch.svelte
<script lang="ts">
  import { fetchTodos } from "../hooks/fetchTodos";
  import { todos } from "../store";

  fetchTodos();
</script>

<h2>onMountの例</h2>
<ul>
  <div>
    {#each $todos as todo}
      <li>
        {todo.id}
        {todo.title}
        <input type="checkbox" checked={todo.completed} />
      </li>
    {:else}
      <div>Loading...</div>
    {/each}
  </div>
</ul>

すると、画像のような形で表示されます。

Vue <=2 で created などに何度も同じロジックを書く羽目になった経験はないでしょうか?Svelte ではコンポーネントが mount されたときのロジックをこうして ts ファイルに別途切り分けることが出来るのです。

beforeUpdate

https://svelte.dev/tutorial/update

https://svelte.dev/docs#run-time-svelte-beforeupdate

beforeUpdate は、state 値が更新される時の前に走らせる callback を返します。他のフレームワークの立ち位置としては、Vue <= 2 の beforeUpdate あたりでしょうか。書き方は以下の通りとなります。

component が初めて mount された際は、onMount より前にここで定義した処理が走る点は注意が必要です。

<script lang="ts">
  import { beforeUpdate } from "svelte";

  beforeUpdate(() => {
    console.log("コンポーネントがアップデートされようとしています");
  });
</script>

afterUpdate

https://svelte.dev/tutorial/update

https://svelte.dev/docs#run-time-svelte-afterupdate

afterUpdate は、component の update された後に走らせる callback を返します。他のフレームワークの立ち位置としては、Vue <= 2 の updated、React の componentDidUpdate あたりでしょうか。書き方は以下の通りとなります。

component が初めて mount された際は、onMount より後にここで定義した処理が走る点は注意が必要です。

<script lang="ts">
  import { afterUpdate } from "svelte";

  afterUpdate(() => {
    console.log("コンポーネントがupdateされました");
  });
</script>

afterUpdate は正直あまり使われている印象がありません。ところが実は便利な関数だったりします。
海外では有名な掲示板 Reddit の r/svelte チャンネルで投稿されたこちらの記事を見てみましょう。React hooks で「mount されるたびに count 数が増える」custom hooks を作ったけど、Svelte ではどう書けばいいのかという質問になります。

https://www.reddit.com/r/sveltejs/comments/q8hj8k/how_to_extract_crosscutting_concerns_in_svelte/

この数で一番 Vote 数を集めている回答がこちらになります。
afterUpdate で count 数を増やせば、store を使わずシンプルで済むよという回答です。
https://www.reddit.com/r/sveltejs/comments/q8hj8k/comment/hgppt3z/?utm_source=share&utm_medium=web2x&context=3

それではコード例を出してみましょう。React でこんな形のコンポーネントがあったとします。
テキストの state 値が変化するたびに count 数が増えるコードです。

const MyComponent = (): JSX.Element => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState('');

  useEffect(() => {
    setCount(count + 1);
  });

  return (
    <input type='text' value={value} onChange={e => setValue(e.target.value)} />
    <p>{count}</p>
  )
};

Svelte では afterUpdate メソッドを使って、以下の書き方をすることで同様の動作を実現が出来ます。

AfterUpdateMemo.svelte
<script lang="ts">
  import { afterUpdate } from "svelte";
  let count = 0;
  let value;
  afterUpdate(() => {
    count++;
  });
</script>

<h2>afterUpdateの例</h2>
<input type="text" bind:value />
<div>テキストを更新した回数: {count - 1}</div>

動きはこんな感じ。

onDestroy

https://svelte.dev/tutorial/ondestroy

https://svelte.dev/docs#run-time-svelte-ondestroy

onDestroy は、component が unmount される(= DOM から消える前)直前に走らせる callback を返します。Vue <= 2 の destroyed や、React の componentWillUnmount 辺りの立ち位置。書き方は以下の通りとなります。

<script lang="ts">
  import { onDestroy } from "svelte";

  onDestroy(() => {
    console.log("コンポーネントは破棄されました");
  });
</script>

store の初期化や event listeners の削除で効果を発揮します。

Actions

Svelte には Actions と呼ばれる機能があります。要素レベルで使う機能になるのですが、bind:this で代用できる場合もあり、あまり活用されていない印象を受けます。

例として「ボタンが押された場合、テキストボックスにフォーカスがあたる」場合を挙げてみます。

1. bind:this を使う

bind:this を使用する場合のコードは、以下のようになります。
ボタンをクリックすると handleClick メソッドが走り、その際 bind:this でバインドした input 要素(ここでは変数 inputNode)にフォーカスさせる流れとなります。
なお handleClick が走った際に、input 要素表示用の state(isEditing)が変化しているため、tick で非同期処理を実行させる必要がある点はご注意を。

WithoutAction.svelte
<script lang="ts">
  import { tick } from "svelte";

  let inputNode: HTMLInputElement;
  let name = "Hello World!!";
  let isEditing = false;

  const handleClick = () => {
    isEditing = !isEditing;
    if (isEditing) {
      tick().then(() => inputNode.focus());
    }
  };
</script>

<h2>Actionsを使わない例</h2>
{#if isEditing}
  <span>
    Name <input type="text" bind:this={inputNode} bind:value={name} />
  </span>
{/if}
<button on:click={handleClick}>{isEditing ? "編集を終える" : "編集する"}</button
>

実際に動作させるとこんな感じ。なお後述する Actions を使用する場合も同じ動作をする形となります。

ただし、まだこの段階では input 要素用でしかメソッドが定義されておらず、同要素を使用するたびにフォーカスイベント用のメソッドを作らなくてはいけません。また bind:this の方法では、「クリックイベントが発生した場合、指定した要素にフォーカスが当たる」を input 要素に限らず使用できる汎用的なメソッドとして切り出そうと思っても、実装に手間が発生してしまう問題が発生します。

実運用時に仕様の変更などで修正する羽目になった場合、似た箇所をすべて洗い流した上で作業する羽目になり非常に良くないやり方です。そんな問題を解決するのが Actions になります。

2. Actions を使用する

公式ドキュメントはこちら。

https://svelte.dev/tutorial/actions

https://svelte.dev/docs#template-syntax-element-directives-use-action

Actions の書き方は以下の通りとなります。use から始まるため、 React hooks の custom hooks や Vue3 の Composition API に近い印象を受けますが、Svelte の Actions は state 管理だけでなく DOM 要素の操作全般で広く使われている印象です。

<!-- parametersを使用しない場合 -->
use:action

<!-- parametersを使用する場合 -->
use:action={parameters}
<script lang="ts">
  export let parameters

  const hoge(node) {
    // 引数のnodeはMountされるDOM要素を表す

    // ここにnodeはMountされたとき、発生させるロジックをここに

    return {
      destroy() {
        // nodeがUnMountしたとき、発生させるロジックをここに
      },
      update(parameters) {
        // parametersの値が更新された際に発生させるロジックをここに
        // ※Actionsにparametersを渡さないときは使用できない
      },
    };
  }
</script>

<div use:hoge></div>

型定義は以下の通り。parameters については、今回の記事で詳しく説明しませんが、Actions に DOM 要素以外の引数を渡したいとき、その引数を定義する場所になります。

action = (node: HTMLElement, parameters: any) => {
	update?: (parameters: any) => void,
	destroy?: () => void
}

これを踏まえて、先程のサンプルコードを Actions を使った形に変更してみましょう。

WithAction.svelte
<script lang="ts">
  import { focusMount } from "../hooks/focusMount";
  let name = "Hello World!!";
  let isEditing = false;

  const handleClick = () => {
    isEditing = !isEditing;
  };
</script>

<h2>Actionsを使う例</h2>
{#if isEditing}
  <span>
    Name <input type="text" use:focusMount bind:value={name} />
  </span>
{/if}
<button on:click={handleClick}>{isEditing ? "編集を終える" : "編集する"}</button
>

<style>
  span {
    margin-right: 1rem;
  }
</style>

focus イベント処理は別途 ts ファイルに切り出しました。これで何度も同じロジックを書く必要がなくなり、修正が必要になった場合も 1 箇所だけ直せば OK になります。
※引数の型定義ですが、Svelte で使用されている rollup でビルドエラーが出るため一旦 any にしました。

hooks/focusMount.ts
export const focusMount = (node: any) => {
  node.focus();
};

Geoff 氏の記事には、Tippy.jsを組み合わせた例も紹介されています。気になる方はぜひ見てみてください。

Actions の使用例については、Kirill Vasiltsov 氏が Svelte Summit 2020 で紹介したスピーチ[1]も参考になります(ちなみに彼は日本在住で Progate 社に勤めている方だったりします)。全編英語ですが、コードを書いている様子を見るだけでも十分参考になるのでぜひ。
https://www.youtube.com/watch?v=9S8GUTcMsgA&ab_channel=SvelteSociety

今回の記事は以上となります。
まだ書ききれてない箇所も多々あるので、また機会があれば Svelte における $ の持つ意味や、公式チュートリアル後半にある機能について語っていきたいなと。

(参考) Svelte の Lifecycle や Actions を使う上で参考になる記事やリポジトリ

参考として今回の記事で参考になった記事およびリポジトリを貼っておきます。

Lifecycle

https://www.newline.co/@kchan/comparing-lifecycle-methods-reactjs-and-svelte--a211a9f9

https://geoffrich.net/posts/svelte-lifecycle-examples/

Actions

https://blog.logrocket.com/svelte-actions-introduction/

両方

最後に Geoff 氏が SvelteKit を使用して作成した、Marvel Comics のデータベース[2]アプリを紹介します。
オープンソースの Svelte アプリが ts で書かれている事例があまりないため、かなり貴重です。アメコミに興味がない人はあまりしっくりこなさそうですが、store の使い方や state 管理が絶妙でとても参考になります。
あまりにも絶妙なので筆者の年末年始の予定に、このリポジトリを読み解くが加わってしまいました。

https://github.com/geoffrich/marvel-by-year

脚注
  1. テキスト版はこちら ↩︎

  2. Marvel Comics は登録が必要ですが、パブリックで使える API を公開しています。 ↩︎

GitHubで編集を提案

Discussion