最近では素のHTMLを記述するのではなく、何らかのUIフレームワークを利用する機会が増えています。たとえばReactやVue.jsが有名です。しかし、こうしたライブラリは大型であり、より高速なUIフレームワークを求められる場合もあります。

そこで今回は、高いパフォーマンスとパワフルさを誇る Solid の使い方を解説します。

Solidとは

SolidはWebアプリケーションのUIを構築するためのフレームワークです。高いパフォーマンスと実用性の高いAPIを持ち、高い生産性を提供しています。JSXが利用でき、AstroやViteといったモダンなツールで利用できます。

なぜSolidを選ぶべきなのか?

Solidが他のフレームワークと比較して優れている点はいくつかあります:

  1. パフォーマンス:Solidは仮想DOMを使用せず、コンパイル時に最適化されたDOMの更新コードを生成します。これにより、ReactやVue.jsなどよりも高速な実行速度を実現しています。
  2. 極小のバンドルサイズ:Solidのコアライブラリはわずか約7KBと非常に軽量です。これはReactやVue.jsと比較して小さく、ページの読み込み時間を短縮できます。
  3. 直感的なリアクティビティ:Solidはリアクティビティを採用しており、必要な部分だけを更新する効率的な仕組みを持っています。これにより、無駄な再レンダリングが発生せず、スムーズなユーザー体験が得られます。
  4. Reactに近い構文:ReactのJSXに慣れている開発者は、Solidの学習曲線が緩やかです。多くの概念が似ているため、スムーズに移行できます。
  5. SSRとHydrationの優れたサポート:Solid SSRはサーバーサイドレンダリングとハイドレーションの機能を組み込みで提供し、シームレスなユーザー体験を実現します。

簡単なサンプル

以下は公式サイトにあるデモです。1秒ごとにカウントアップするコンポーネントです。

import { onCleanup, createSignal } from "solid-js";
import { render } from "solid-js/web";

const CountingComponent = () => {
  const [count, setCount] = createSignal(0);
  const interval = setInterval(
    () => setCount(count => count + 1),
    1000
  );
  onCleanup(() => clearInterval(interval));
  return <div>Count value is {count()}</div>;
};
カウントアップのアニメーション

Solidの使い方として、ReactでいうところのuseStateはcreateSignalになります。初期値(今回は 0)を渡すのもReactと同じです。また、その返却値は変数ではなく、関数になります。そのため、実際に値を表示する際には count() という風に関数として呼び出します。値を変更する関数 setCount の使い方はReactと同じです。

onCleanup はコンポーネントがアンマウントされた際に実行される関数です。ここでは、setIntervalで作成したインターバルをクリアする処理を行っています。同様に onMount という関数もあり、コンポーネントがマウントされた際に実行される処理を記述できます。

始め方

TypeScriptとともに始める場合には、以下のコマンドが使えます。

npx degit solidjs/templates/ts my-app
cd my-app
npm install
npm run dev

これで http://localhost:3000 にてSolidアプリケーションが起動します。

Solid起動画面

変数を扱う

画面上で変更される変数は createSignal で作成します。これは、Reactの useState に相当します。ただし、Solidでは変数の値を取得するために関数を呼び出す必要があります。また、変数を変更する関数は setCount のように、変数名の前に set をつけた形で作成します。

const [count, setCount] = createSignal(0);

return (
  <p>
    Hello Vite + { count() }
  </p>
);

このままでは 0 と出るだけなので、 setCount を使って変更する処理を追加します。

<button onClick={() => setCount(count => count + 1)}>Increment</button>

これでカウントアップするイベントが追加されます。

Solidのリアクティビティの仕組み

ReactとSolidの大きな違いの一つは、リアクティビティの実装方法です。ReactではJSXの中で使用されている値が変更されると、コンポーネント全体が再レンダリングされるのに対し、Solidでは変更された値に依存する部分だけが更新されます。

例えば、以下のコードでは:

function Counter() {
  const [count, setCount] = createSignal(0);
  const [name, setName] = createSignal("John");

  return (
    <>
      <div>Count: {count()}</div>
      <div>Name: {name()}</div>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </>
  );
}

setCountが呼ばれたとき、Reactではコンポーネント全体が再評価されますが、Solidではcount()を使用している<div>Count: {count()}</div>の部分だけが更新されます。これにより、不要な再計算や再レンダリングが削減され、パフォーマンスが向上します。

変数の変更を監視する

createEffect を使って、変数が変更されたときに処理を実行できます。これは、Reactの useEffect に相当しますが、依存配列を明示的に指定する必要がありません。Solidは自動的に依存関係を追跡します。

createEffect(() => console.log("count =", count()));

これは単純な例ですが、二つの変数 userloggedIn がある場合、 user が変更されたときに loggedIn を変更する処理を追加することもできます。

createEffect(() => {
  setLoggedIn(user() !== null);
});

非同期処理の扱い

データを外部サービスから取得する場合には createResource を使います。これは、Reactの useEffectuseState を組み合わせたようなものですが、より宣言的で使いやすいAPIを提供しています。

const user = createResource(async () => {
  const response = await fetch("https://api.example.com/user");
  return response.json();
});

表示する際には、これらの状態を利用してUIを構築できます:

return (
  <div>
    {user.loading && <p>読み込み中…</p>}
    {user.error ?
      <p>エラー: {user.error.message}</p>
      :
      <p>{user()?.name}</p>
    }
  </div>
);

計算処理のキャッシュができるcreateMemo

createMemo を使うことで、計算結果をキャッシュできます。これは、Reactの useMemo に相当しますが、依存配列を明示的に指定する必要がなく、自動的に依存関係を追跡します。 createMemo を使うことで、 count が変わっていない時には計算処理が行われず、パフォーマンスを向上させられます。

const double = createMemo(() => count() * 2);

より複雑な変数を管理するストア

createStore を使うことで、オブジェクトや配列を管理できます。これは、Reactの useReducer に相当しますが、より直感的なAPIを提供しています。以下はそのコード例です:

import { createStore } from "solid-js/store";

const [user, setUser] = createStore({
  name: "John Doe",
  age: 30,
  address: { city: "New York", country: "USA" }
});

return (
  <div>
    <p>Name: {user.name}</p>
    <p>Age: {user.age}</p>
    <p>Address: {user.address.city}</p>
    <button onClick={() => setUser("age", user.age + 1)}>Increase Age</button>
    <button onClick={() => setUser("address", "city", "Los Angeles")}>
      Move to LA
    </button>
  </div>
);

Solidのストアは、ネストされたオブジェクトの更新も簡単に行えます。上記の例では、"address", "city"のように、パスを指定するだけで深くネストされたプロパティを更新できます。

SSR(サーバーサイドレンダリング)

Solidはサーバーサイドレンダリング(SSR)にも対応しています。以下はサーバー側のコード例です。

import { renderToString } from "solid-js/web";

const App = () => <div>Hello World</div>;
renderToString(() => <App />);

クライアント側では hydrate を使ってラップします。

import { hydrate } from "solid-js/web";

hydrate(() => <App />, document);

サーバーとクライアントでのレンダリングを判定するため、 isServer を使うこともできます。

import { isServer } from "solid-js/web";

if (isServer) {
  // サーバーでのみ実行
} else {
  // ブラウザでのみ実行
}

Reactからの移行

ReactからSolidへの移行を検討している開発者にとって、多くの概念は似ているため、学習曲線は比較的緩やかです。以下に主な違いをまとめます:

React Solid 主な違い
useState createSignal Solidでは値を取得するために関数呼び出しが必要
useEffect createEffect Solidでは依存配列が不要、自動で依存関係を追跡
useMemo createMemo Solidでは依存配列が不要、自動で依存関係を追跡
useContext createContext 基本的な使い方は類似
仮想DOM コンパイル時最適化 Solidは実行時ではなくコンパイル時に最適化
コンポーネント全体再評価 細粒度のリアクティビティ Solidは変更された部分だけを更新

まとめ

Solidは、React風の使い勝手でありながら、より高パフォーマンスなWebアプリケーションが開発できるモダンなUIフレームワークです。仮想DOMを使用せず、コンパイル時最適化と細粒度のリアクティビティによって、高速で効率的なアプリケーションを構築できます。

特に以下のような場合にSolidの採用を検討すると良いでしょう:

  • パフォーマンスが重要なアプリケーション
  • バンドルサイズを小さく抑えたい場合
  • 複雑なリアクティブな状態管理を必要とするプロジェクト
  • Reactから移行を検討している開発者チーム

SSRにも対応しているので、クライアント・サーバーともにSolidの手法で開発できるのは大きな魅力です。

ReactやVue.jsとはまた違う面白さがあるので、ぜひ試してみてください。

Solidの公式サイト