🚀

Svelte/SvelteKit の地味に好きなところ

2024/09/07に公開3

普段から Svelte / SvelteKit を使っているので、その地味に好きなところを紹介します。
また、微妙なところも最後に紹介します。

Who are you?

  • ryoppippi
  • Frontend は Svelte メインで書いている
  • React も書くが、少なめ
  • Astro は書いている
  • Solid でも簡単なものを作ったことがある
  • Vue3 はほぼ触ったことがない

Svelte の好きなところ👍

直感的に書けるところ

Svelte は直感的に書けるところが好き。
見た目も html に近い。
エディタの支援もしっかりしているので、書いていてストレスがない。

速い、軽い、うまい

結構適当に作っても、speedInsight でいい点数を取りやすい。
また、ビルド後のファイルサイズも小さいので、デプロイもしやすい(特に Cloudflare Pages との相性がいい)。

型安全がしっかりしているところ

tsx と比較して型安全が疎かであるように思われているが、Svelte は型安全がしっかりしている。

<script lang='ts'>
  const fruit: 
    | 'apple'
    | 'orange'
    | 'banana'
    | 'grape' = 'apple';
</script>

{#if fruit === 'apple'}
  <p>🍎</p>
{:else if fruit === 'orange'}
  <p>🍊</p>
{:else if fruit === 'banana'}
  <p>🍌</p>
{:else if fruit === 'grae'} <!-- grape が typo しているのでエラーになる -->
  <p>🍇</p>
{:else}
  <!-- fruit は never なので cast が必要 -->
  <p style='color: red'>Unknown fruit {fruit as unknown as string}</p> 
{/if}

このように、ちゃんと型安全が効いている。
Svelte5 からは template 部分でも TypeScript の記法が使えるようになったため、より柔軟性の高い記述が可能になった。

もちろん要素の属性に渡す値や関数も型安全である。

また、後述する SvelteKit ではより面白い型安全機能が追加されている。

class:name/ style:name が便利なところ

Svelte では、通常の class='foo' に加えて、class:foo という書き方ができる[1]
加えて、class:foo={true} という書き方で条件を指定することで、クラスの付与を制御できる。

<p 
  class='foo'
  class:bar
  class:baz={true}
  class:qux={false}
/>

これは tailwindcssUnoCSS などの utility-first CSS フレームワークとの相性がいい。

<p 
  class='text-white'
  class:text-red={isDanger}
  class:text-blue={isInfo}
  class:text-green={isSuccess}
  class:text-yellow={isWarning}
/>

tsx で似たようなことをやろうとすると、clsx 等の追加のライブラリが必要になり、そのぶん runtime cost が増える。
標準機能でこれができるのは、とても便利。
おまけに可読性が高い。

ちなみに、最近 Astro にも class:list が追加されて、clsx が不要になった[2]

stylestyle:name={foo} のように書くことができる[3]

<p 
  style='color: red'
  style:margin-top='{foo}rem'
  style:background-color={darkMode ? 'black' : 'white'}
/>

この書き方は、特に css variables を使うときに便利である。

<script>
  const color = '#ff3e00';
</script>

<p style:--color={color} />

<style>
  p {
    color: var(--color);
  }
</style>

こんな感じで、外部変数を既存の css に適用することができる。

style タグが使いやすいところ

Svelte では、style タグ内で書いたスタイルは、そのコンポーネント内でのみ有効である。
いわゆる scoped スタイルである。

<style>
  p {
    color: red;
  }
</style>

もちろん、global なスタイルも書ける。

<style global>
  :global {
    body {
      background-color: black;
    }
  }
</style>

また、未使用のスタイルがあれば警告を出してくれるし、ビルド時に未使用のスタイルは自動的に削除される。
とても便利。

each blocks に :else が使えること

本当に地味なポイントだが、each blocks に :else が使えて嬉しい。

{#each items as item (item.id)}
  <p>{item}</p>
{:else}
  <p>No items</p>
{/each}

for-else 構文ってどんな言語にあるんだろう。
Python とか Zig とか?

Rune が使いやすいところ

Svelte5 で導入される rune が使いやすい。
rune についてここで語ると長くなるので、詳しくは色々記事をご覧ください。
ただ、正式リリースまではAPIが安定しないのでご了承ください。

日本語資料:
https://baseballyama.github.io/techfeed-20240424-svelte5/1
https://docs.google.com/presentation/d/14wc-XSfmXxfHu-2YMM8gJHANzZbAdkS8YSwS-rvf48U/edit
https://zenn.dev/tomoam/scraps/375fb71c09fe0f

英語資料(このdemoが本当にわかりやすいので、字幕をつけて見てほしい):
https://www.youtube.com/watch?v=_SpO5T96AYY
https://svelte-5-preview.vercel.app/docs/runes

自分は昨年発表されてからずっと rune を使っているが、かなり使いやすい。
正直 Svelte4 以前には戻れない。それくらい書き味が良い。

見た目は VueRef であり、省略記法な ReactuseState であるが、コンパイラを経由することで書き心地とパフォーマンスを両立している。

他にも snippet 構文が最高だったり、$props() を用いた型安全な props の宣言が可能になったりと、 Svelt5 はとても良いものだが、いかんせんリリースが遅れに遅れているのが残念。
年度末の Advent Calender では思う存分 rune についての記事を書きたい。

<script lang='ts'>
  import type { ComponentProps } from 'svelte';
  type Props = {count: number} & ComponentProps<typeof HTMLDIVElement>;

  const { cont, ...rest }: Props = $props();
</script>

{#snippet countDiv(count: number)}
  <div {...rest}>Count: {count}</div>
{/snippet}

{@render countDiv(count)}

SvelteKit の好きなところ👍

Zero-effort type safety

https://svelte.jp/blog/zero-config-type-safety

SvelteKit では +page.server.ts+page.svelte というファイルを用いて、サーバーの処理とクライアントの処理を分離することができる。
Remix でいうところの loadercomponent に相当するものである(厳密に言えば +page.server.ts には actions も書ける)。

すごいのは、この2つのファイルにまたがっているデータのやりとりが自動的に型安全になることである。
typeof load みたいなことをする必要がなく、+page.server.tsload 関数の戻り値を変更すると即座に +page.sveltedata の型も変わる。

form_3
+page.server.ts の戻り値を変更すると、+page.sveltedata の型も自動的に変わる

また、path の params なども自動的に型安全にしてくれる[4]
自分で、型を指定する必要はない。

// src/routes/blog/[slug]/+page.server.ts
export async function load({ params }) {
  // slug は string であることが型安全に保証される ↓
  const post = await getData(params.slug); 

  if (post) return post;

  return error(404, 'Not found');
};

コードを書けば自動で型がついてくれる体験は病みつきになる。

Web 標準技術を駆使しているところ

SvelteKit は、Svlte の文法こそ独自であるものの、ベースとなる技術は Web 標準技術を駆使している。

https://kit.svelte.jp/docs/web-standards

例えば、fetch関数、Request クラス、Stream APIsForm Actions などなど。
ここ1年ほど React 界隈でやっと 'Form Actions' やら Progressive Enhancement が話題になってきたが、SvelteKit はそれに先駆けて Web 標準を謳っていた印象がある。
これについても過去に記事を書いたので、興味があれば読んでみてください。

https://zenn.dev/ryoppippi/articles/aea8dcbc21c39e

Server / Client がファイルベースで分離されているところ

先ほども述べたが、SvelteKit では +page.server.ts+page.svelte というファイルを用いて、サーバーの処理とクライアントの処理を分離することができる。
また、.server がついているファイルはクライアント側で読み込むことができない。
環境変数も、PUBLIC_ がついているものだけがクライアント側で読み込むことができる。
クライアント側に不要な情報が漏れることを防ぐ仕組みができていることは、開発する上で安心感を与えてくれる。

https://zenn.dev/ryoppippi/articles/8addfe62eb4d3e

Svelte / SvelteKit の微妙なところ🤔

色々あるが...

対応しているライブラリが(比較的)少ない

React に比べると、対応しているライブラリが少ない。
特に UI 周り。 shadcn-sveltemelt-ui など使い勝手のいいものはそこそこあるが、まだまだ少ない。
React AriaSvelte 版があればいいのに...。

また、vueUse に匹敵するような便利関数詰め合わせライブラリもまだまだ発展中。
そもそも Svelte5 でこれまでの store から rune への移行の過渡期であるため、安定してこないところもある。
とはいえ、ある程度素の JavaScript ライブラリが使えるので、そこまで困ることはない。

route が型安全にならない (SvelteKit)

fetch 関数を使う時や、a タグでリンクを貼るときの文字列が型安全にならない。
一応、存在しないpathを指定すると、ビルド時にエラーが出るので 気づくことはできるが、開発中に型安全であると嬉しいなと思う(Next.jsのtypedRoutes みたいなもの希望)。
現状ではvite-plugin-kit-routes というプラグインで route を型安全にすることができるが、これが標準であると嬉しい。
https://www.kitql.dev/docs/tools/06_vite-plugin-kit-routes

Server Endpoint の Response が型安全にならない (SvelteKit)

Server endpoint の結果も型安全になっていない(普通に fetch 関数で叩いてデータを取得しているだけなので)。
現状、Server Endpoint の型安全を保証したいなら、trpcHono RPC と組み合わせる必要がある。
上で述べた通り、SvelteKit は色々と型安全を頑張っているので、もう一踏ん張りしてほしい。
(そもそも Server Endpoint を使う必要はあるのか、全部 +page.server.tsload 関数内に書いてしまえばいいのでは? という意見もある)

tsx じゃない

個人的には tsx があまり好きになれないのだが、これは好みの問題ではない。
ts/tsx でないことの最大のデメリットは、マクロ系のライブラリが使えないこと。
例えば unplugin という、ビルド時に便利な処理をしてくれるプラグインたちがいるのだが、彼らは大抵 ts/tsx の処理に特化している。
そのため、vue / svelte / astro などの非 ts/tsx なフレームワークの対応が遅れがちになる。
(個人的には unplugin-macros) が使えないのがたまに辛いこともある)
とはいえ、必要な処理を ts ファイルに切り出せばいいので、そこまで困ることはない。データの処理であれば +page.server.ts に逃すこともできるので。
まあ、本当に必要ならば自作するのですが...。

まとめ

ざっくり Svelte / SvelteKit の好きなとこを紹介した。

脚注
  1. https://svelte.jp/docs/element-directives#class-name ↩︎

  2. https://docs.astro.build/en/reference/directives-reference/#classlist ↩︎

  3. https://svelte.jp/docs/element-directives#style-property ↩︎

  4. https://kit.svelte.jp/docs/routing#page-page-server-js ↩︎

Discussion

ShoutaShouta

記事を公開していただき、ありがとうございます!

質問なのですが、「直感的に書けるところ」というのはどのような点を指しているのでしょうか?
まず前提として私は「Reactはある程度書けるがSvelteには全く詳しくない、でも興味は持っている」という状態です。

私の印象だと、

  • propsの宣言がexportで行われる(exportが外からの入力を表す)
  • $: を使用するとリアクティブになる(合ってるか分からないけど多分そう?)

など、他ではあまり見られない書き方が多くいです。

なかなか非直感的で辛そうだなーと勝手ながら思っていたのですが、意外と触ってみるとそんなことはないのでしょうか?

Svelteってユーザーからの評価はかなり高い印象があって、ずっと興味がありました。
個人的にReact(JSX)が好きなのもあり、Svelteの書き方が気持ち悪く見えるのがネックで後回しにしてたのですが、意外にそんなことないというのであれば試してみたいなと思った感じです...!

ryoppippiryoppippi

コメントありがとうございます!

「直感的に書けるところ」 という意味の真意は

  • HTMLの書き方に似ているので、個人的には親しんでいるスタイルだから
  • 少ない記述で動く
  • stateを宣言するにもどのhookを使うか迷う必要なく、ただ値を宣言するだけでよい(メモ化も勝手にやってくれるし、値も代入するだけでよい)

といったところです。
3つめの部分に関して具体的なコードを書くと、Reactでは

const [a, setA] = useState(0);
const [b, setB] = useState(0);
const x = a + b;

return default Component(
  <div>
    <p>a: {a}</p>
    <p>b: {b}</p>
    <p>x: {x}</p>
    <button onClick={() => setA(a + 1)}>Increment A</button>
    <button onClick={() => setB(b + 1)}>Increment B</button>
  </div>
);

と書くところを、Svelteでは

<script>
let a = 0;
let b = 0;

$: x = a + b;
</script>

<div>
  <p>a: {a}</p>
  <p>b: {b}</p>
  <p>x: {x}</p>
  <button on:click={() => a++}>Increment A</button>
  <button on:click={() => b++}>Increment B</button>
</div>

で書ける、ということです(Reactを悪く言う意図はありません)。

とはいえ、export を多用する、$: でリアクティブになるというのは、Svelte に初めて触れる方からはとっつきにくいと言われる意見ですね。
自分はなれてしまいましたが。

ただ、Svlete4までのReactivityには一部の直感さを優先した結果起きてしまった非直感さも多くあります。
例えば、top-levelで宣言した変数しかリアクティブにならないこともその一つです。
加えて、 export let とかは受け手側は値を変更しないのに let を使わざるをえないのが違和感がありそうです(実際自分もそうでした)。

それらはだいぶSvelte5で解消されると思います。

上に示したコードは、Svelte5で書くと

<script>
let a = $state(0);
let b = $state(0);

const x = $derived(a + b); // 再代入しないので `const` でよい
</script>

<div>
  <p>a: {a}</p>
  <p>b: {b}</p>
  <p>x: {x}</p>
  <button onclick={() => a++}>Increment A</button>
  <button onclick={() => b++}>Increment B</button>
</div>

Repl

と書けるようになります。

また、export let の代わりに $props が導入されるので、React に似た感覚で props を受け取ることができるようになります。

参考 Repl

なので、Svelte5 になると、React になれている方も、Svelte を始める心理的なハードルが下がると思います。
ただ、まだ正式リリースになっていないので、チュートリアルが古いままなので勉強しづらいですね....
Svelte5のリリースはおそらく数ヶ月先だと思いますが...

ここら辺の資料を眺めていただければなと思います!

ShoutaShouta

返信ありがとうございます!

とはいえ、export を多用する、$: でリアクティブになるというのは、Svelte に初めて触れる方からはとっつきにくいと言われる意見ですね。

そうですよね。同じ理由で触るのを躊躇ってました。

それらはだいぶSvelte5で解消されると思います。

また、export let の代わりに $props が導入されるので、React に似た感覚で props を受け取ることができるようになります。

なるほど!Svelte5のruneについて、ずいぶん前から知ってはいましたが、「なんとなく使いやすくなるんだなー」程度の浅い認識しかありませんでした...。
自分がSvelteを避けてた理由の大半が解決されているのであれば、触ってみたくなりますね。

改めて、わざわざ回答していただきありがとうございました!
触ってみます!