SvelteのLifecycleメソッドとActions機能を改めて確認する
はじめに
この記事は Svelte Advent Calendar 2021 の 22 日目の記事です。筆者にとって本アドベントカレンダーでは 2 つ目の記事となります。
最近 Svelte の Contributor の 1 人である Geoff Rich 氏が以下の 2 記事を投稿していました。
筆者は React(hooks ベース)を書くことも多いのですが、React のコードから Svelte に置き換える際、少しでも hooks 特有の書き方が載っていると書き方の違いで結構戸惑うことが多く困っていました(あれ?React だとすんなり書けるけど、Svelte だとこの時どうしたらいいのだろう?みたいな気持ちです。)。
そんな感じでモヤモヤしたまま日々を過ごしてたのですが、そんなとき上記記事が投稿されていたので読んでみたところ、Svelte の基礎がかなり抜けていることに気付かされました。
今回は上記記事を読んで学んだことを基に、Svelte の Lifecycle
や Actions
について改めて復習していこう的な記事になります。元記事はすべて生の JavaScript で書かれていますが、可能な限り TypeScript(以下 ts)に置き換えています。
開発環境
PC: MacBook Pro(Apple M1, 2020)
OS: macOS Monterey 12.0.1
サンプルコード
REPL について
まず Svelte コードを書く上で、便利なツールを紹介しておきます。
REPL
は Svelte の公式 PlayGround になります。React や Vue の公式にはこの手の機能はないので、Svelte の魅力の 1 つだと個人的には思っています。またアカウント登録しておくことで、コードの共有も出来ます。
チュートリアルもこの REPL
を使って、進める方式になっています。ブラウザ上で色々試しながら勉強できるのは、かなりいいですよね。
Lifecycle
Svelte には Lifecycle
を司る関数として、onMount
/onDestroy
/beforeUpdate
/afterUpdate
/tick
の 5 つがあります。今回は、Geoff 氏の元ネタ記事に従い、tick
以外の関数を紹介します。
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
してみます。
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 は $
をつけて取得することが出来ます。
<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
beforeUpdate
は、state 値が更新される時の前に走らせる callback を返します。他のフレームワークの立ち位置としては、Vue <= 2 の beforeUpdate
あたりでしょうか。書き方は以下の通りとなります。
component が初めて mount
された際は、onMount
より前にここで定義した処理が走る点は注意が必要です。
<script lang="ts">
import { beforeUpdate } from "svelte";
beforeUpdate(() => {
console.log("コンポーネントがアップデートされようとしています");
});
</script>
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 ではどう書けばいいのかという質問になります。
この数で一番 Vote 数を集めている回答がこちらになります。
afterUpdate で count 数を増やせば、store を使わずシンプルで済むよという回答です。
それではコード例を出してみましょう。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
メソッドを使って、以下の書き方をすることで同様の動作を実現が出来ます。
<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
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
で非同期処理を実行させる必要がある点はご注意を。
<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 を使用する
公式ドキュメントはこちら。
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
を使った形に変更してみましょう。
<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
にしました。
export const focusMount = (node: any) => {
node.focus();
};
Geoff 氏の記事には、Tippy.jsを組み合わせた例も紹介されています。気になる方はぜひ見てみてください。
Actions
の使用例については、Kirill Vasiltsov 氏が Svelte Summit 2020 で紹介したスピーチ[1]も参考になります(ちなみに彼は日本在住で Progate 社に勤めている方だったりします)。全編英語ですが、コードを書いている様子を見るだけでも十分参考になるのでぜひ。
今回の記事は以上となります。
まだ書ききれてない箇所も多々あるので、また機会があれば Svelte における $
の持つ意味や、公式チュートリアル後半にある機能について語っていきたいなと。
(参考) Svelte の Lifecycle や Actions を使う上で参考になる記事やリポジトリ
参考として今回の記事で参考になった記事およびリポジトリを貼っておきます。
Lifecycle
Actions
両方
最後に Geoff 氏が SvelteKit を使用して作成した、Marvel Comics のデータベース[2]アプリを紹介します。
オープンソースの Svelte アプリが ts で書かれている事例があまりないため、かなり貴重です。アメコミに興味がない人はあまりしっくりこなさそうですが、store の使い方や state 管理が絶妙でとても参考になります。
あまりにも絶妙なので筆者の年末年始の予定に、このリポジトリを読み解くが加わってしまいました。
Discussion