🧊

Svelte(Kit)を使ってて気になるポイント

Svelte(Kit)に対しては、基本的に"推し"なスタンス。

けど、今回はあえて気になるポイントも書いておこうかと。めちゃめちゃ不満ってほどでもないけど、気になるってところ。

ちなみに、SSRは基本的にしない(できない)用途の開発ばっかりやってる勢なので、観点には多少の偏りがあるはず。

Svelte: 1ファイル1コンポーネント縛り

`.svelte`ファイルからは、コンポーネント1つしか返せないという制約があり、あれがたまに息苦しいという話。

つまりJSXでは可能だった、Named exportで似たような用途のコンポーネントをまとめて返す・・とかができない。

是非はさておき、Propsの型すらexportできない。

もしその対象が静的なHTML片なのであれば、`#if`で分岐してもいいけど、これはちがうんよな〜〜。

<script>
  export let fragmentKey;
</script>

{#if fragmentKey === "A"}
<div>A</div>
{/if}

{#if fragmentKey === "B"}
<div>B</div>
{/if}

他にも、ちょっとしたフラグメントをインラインで変数化して置いておくこともできなくて、これはずっと前からRFCが浮いてる状態。

Inline components by Rich-Harris · Pull Request #34 · sveltejs/rfcs · GitHub

あとはEarly returnもできないので、唐突にJSX(というかReact)が恋しくなる瞬間がくる時がある。(SolidもJSXなのにEarly returnはできないけども)

まあたまに息苦しくはなるものの、Hooksからコンポーネント返すとかいうアクロバティックな技も禁止できるっていうのは、一周回って安全!やはり正しい!っていう気持ちもあり、まあ受け入れるかこのトレードオフを・・・って感じ。

まあファイル名を見ただけで、その中に1コンポーネントだけ入ってることが保証されるってことであり、なんでこんなとこにロジック書いた?!みたいな事故も起きないってことやし、安全第一で。

ちなみに、`index.js`みたいなのを置いて、その中で複数のコンポーネントをre-exportすることはできる。

export { default as Comp1 } from "./comp1.svelte";
export { default as Comp2 } from "./comp2.svelte";

Svelte: Storeまわり

少し正規化したい規模のプロジェクトで、こういうStoreを作って返した場合。

// my-store.js

const count = writable(0);
const unit = writable(5);

return {
  state: {
    count: { subscribe: count.subscribe },
    unit: { subscribe: unit.subscribe },
  },
  action: {
    /* ... */
  },
};

気持ちとしては、似たようなものを固めて`export`したいというもの。

で、まず気になるのは、StoreをReadOnlyで`export`するために必要な`subscribe`だけを返すこの一連の流れが冗長ってこと。(それだけやる関数を作ればいいのはわかるけどそういうとこよ)

次に、`.svelte`のテンプレートで`foo`という変数名のStoreを、`$foo`としてアクセスできる便利なアレを使う時。

<script>
  import { state, action } from "../my-store";
</script>

{$state.count}

とは書け・・・ない。もちろん`{state.$count}`とも書けない。

<script>
  import { state, action } from "../my-store";

  const { count } = state;
</script>

{$count}

というように直で`$`をつける必要があり、これが塵積で不便やなって。

ならいっそ`state`という層を設けなければよいのでは〜っていうのは一理あるけど、なんか妥協っぽくてこれはこれで気になる。

import { count, action } from "../my-store";

というところで、制約が地味に息苦しいな〜という。いやまあ些細な問題ではあるけども。

そもそも、リアクティブ系のシンタックスが`let`と`store`と2種類あるのも地味に脳内リソースを消費する一因になってるかも。

Svelte: TSサポートの弱さ

たとえば、こういうコンポーネントを作りたい場合。

<div>
  <label for="email">メールアドレス</label>
  <input id="email" type="email" />
</div>

JSX系のコンポーネントなら`Props`を`InputHTMLAttributes & { label: string }`みたくできる。

が、Svelteではそういうことができないので、

  • `$$props`でまとめて受け取るが型を諦める
  • ぜんぶ手動で属性値を指定し、`click`などのイベントも全部手動でdelegateする

という2択を迫られる。これは普通に面倒くさい。

More elegant prop typing · Issue #6067 · sveltejs/svelte · GitHub

Svelte: Portalほしい

過去に何度もIssueになっては閉じられを繰り返してる。

自分でそれらしい挙動を実装できるのはそのとおりではあるけど、あってもよくない?って思ってる。

[feature] Add `Portal`s to Svelte · Issue #7082 · sveltejs/svelte · GitHub

こういうのは、内部実装や挙動のクセをちゃんと把握してないと、マトモなものが書けないので。

SvelteKit: SSRする場合、シンタックスが冗長になる

SvleteKitはSSRすることを軸足に置いたメタフレームワークなので、採用を決めたその瞬間からたとえば、

<script>
let value = localStorage.getItem("VALUE");
$: localStorage.setItem("VALUE", value);
</script>

みたいなコードが書けなくなる。

記述量が少なくて済むのがSvelteのいいところなのに、Kitに乗った瞬間からお行儀のいいIsomorphicなコードを要求される。
`onMount()`で囲んで・・`value`をStoreに逃がして・・と、一気にコードが簡潔じゃなくなる。

まあこれはSvelteKitに限らず、いわゆるメタフレームワーク全部がそうなんやけど、Svelteの場合は元が元なだけに、余計に気になってしまうって話。

幸い`export const ssr = false`ってすればいいだけではあるけども。

SvelteKit: `load()`を`{#await}`したい

個人的にはSapperの頃からずっと思ってるやつ。

How to avoid waiting for +page.js to load the data (on the client side) · sveltejs/kit · Discussion #8640 · GitHub

ローディング表示もなく単に白い画面で待たされるの、不安すぎません?これだからSSRはな・・ってなるポイントでもある。

SvelteKit: `+layout.svelte`でNamed slotが使えない

つまり、各ページ側からは、単一の`slot`にしか手が出せないということ。

ページごとに外側のレイアウトに対して表示を制御したい場合(特別なヘッダ出したいとか、`z-index`を超えたいとか)は、

  • 自分でレイアウト用のコンポーネントを作って自分で使う
  • いわゆる`Portal`相当のテクニックを使う
    • Svelte本体には存在しない

という必要がある。ちょっと不便。

SvelteKit: 特殊なファイル名なせいで

`+page.svelte`とか、`(app)`みたいなやつのこと。

zshとかシェルの問題かもしれんけど、`vi +page.svelte`ってやるとダメで、`vi ./+page.svelte`のようにしないとファイルが開けなくて不便。
(これだからVSCodeは!)

SvelteKit: SSRがデフォルトか〜

って言いたいだけのところはある。

Support CSR as first class citizen in kit · Issue #8631 · sveltejs/kit · GitHub

まぁ一括ではなくページ別にこの設定ができるのは、数あるメタフレームワークの中でもSvelteKitだけだと思う(SolidStartは一括指定しかなかったはず)ので、まあやればいいだけの話。

というわけで

まあ気になるポイントはあるけど、どれもクリティカルではなく、

  • Write less code最高
  • `{#await}`ならびに各種シンタックスが便利すぎ
  • `on:click`とだけ書けばイベント上層に通せるの最高
  • 双方向バインディングも楽すぎ
  • ページ別にSSG/SSR/CSR選べるのえらい

みたいなところで、まあこれからも引き続きヘビーに使っていくとは思う。

ただここに書いてきたちょっとした不満(特にストアまわり)により、思ってたより大きな規模のアプリには採用しないかも?っていう兆しを感じてる今日このごろ。次にSPA(s)をやる案件がきたら、SolidStart w/ `ssr: false`しちゃうかも。