🌀

SolidJSの開発体験の良さを知ってほしい

2024/12/24に公開

はじめに

2024年12月5日、React 19が安定版となり、世間はReactの話題で持ち切りです。Reactへの関心が高まっている今、あえてここで私の大好きなSolidJSの宣伝をさせてください。

SolidJSとは

SolidJSは宣言的UIフレームワークです。ReactやVue.jsと同様の役割を持ちますが、以下のような特徴があります。

  • Signalベースのリアクティビティ
  • 仮想DOM不使用
  • JSXによるコンポーネントの記述

詳しくは公式サイトをご覧ください。

"Reactでいいじゃん"...本当に?

「ReactやVue.jsと同様の役割を持つ」ならReactでいいじゃん

そう思ったそこのあなたに向けてこの記事を書いています。
確かにReactは素晴らしいフレームワークです。コミュニティが大きく、多くの実装例とライブラリが存在します。SolidJSの方が少しパフォーマンスは良いですが、大抵のユーザーはブラウザ上でこの差を知覚することはありません。

ではなぜ私がSolidJSを推しているのか。それはSolidJSの開発体験の良さが大好きだからです。

Fine-Grained Reactivityが勝手に上手くやってくれる

SolidJSのReactivityはSignalを中心に構築されています。Signalは値を保持し、それが変更されると自動的に依存関係(effectsとDOM更新)を再実行します。依存配列を手動で指定する必要はありません。

SolidJS
import { createSignal, createEffect } from "solid-js";

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

// シグナルを監視するEffectを作成
createEffect(() => {
  console.log("カウントが変わった:", count());
});

setCount(1); // Effectが再実行される
setCount(2); // Effectが再実行される

これを実現するSignalの具体的な実装や、動的な依存関係の更新によるきめ細かな更新など、語りたいことは尽きないのですが、これらは先駆者様の記事で詳細に解説されているので、そちらをご覧ください。

同期的な状態更新が直感的

突然ですが、reactで簡単なカウンターコンポーネントを実装してみました。
ボタンをクリックするたびにカウントが増え、カウントの2倍の値が表示されるだけのコンポーネントです。また、クリック時コンソールにカウント、2倍の値、ボタンのテキストを表示します。

Reactでの例

React
import React, { useState, useMemo, useRef } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const doubled = useMemo(() => count * 2, [count]);
  const buttonRef = useRef();

  return (
    <button
      ref={buttonRef}
      type="button"
      onClick={() => {
        setCount(count + 1);
        console.log(
          `count: ${count}, doubled: ${doubled}, textContent: ${buttonRef.current.textContent}`,
        );
      }}
    >
      {doubled}
    </button>
  );
};

export default App;

このボタンを3回クリックしたときに、コンソールに表示される値がどうなるか予想してみてください。

...正解は以下の通りです。

クリックして結果を表示
(1クリック目) count: 0, doubled: 0, textContent: 0 
(2クリック目) count: 1, doubled: 2, textContent: 2 
(3クリック目) count: 2, doubled: 4, textContent: 4 

予想通りの結果でしたでしょうか?

SolidJSでの例

次に同様のコンポーネントをSolidJSで実装してみましょう。

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

function Counter() {
  const [count, setCount] = createSignal(0);
  const doubled = () => count() * 2;
  const [buttonRef, setButtonRef] = createSignal<HTMLButtonElement>();

  return (
    <button
      type="button"
      ref={setButtonRef}
      onClick={() => {
        setCount((c) => c + 1);
        console.log(count(), doubled(), buttonRef()?.textContent);
      }}
    >
      {doubled()}
    </button>
  );
}

render(() => <Counter />, document.getElementById("app")!);

こちらも同様に3回クリックすると、コンソールには以下のように表示されます。

(1クリック目) count: 1, doubled: 2, textContent: 2
(2クリック目) count: 2, doubled: 4, textContent: 4
(3クリック目) count: 3, doubled: 6, textContent: 6

setCountでカウントを更新した直後count()doubled()が、更新後の値を返しています。また、buttonRef()?.textContentも更新後のボタンのテキストを返していますね。

SolidJSのReactivityは同期的な更新を実現しています。Signalの更新は即座に反映され、その後の処理は同期的に行われます。
ReactとSolidJSの挙動のどちらが"優れているか"は一概には言えません。Reactでの挙動は、"setCountによる更新を非同期でコミットするまで、更新後の値を取得しない"という点で、一貫した挙動と言えます。コンソールの出力だけ見れば、SolidJSでの出力と1クリック分ずれているだけです。
そのうえで、SolidJSでの同期的な状態更新は、直感的に理解しやすく、デバッグもしやすいと私は感じます。

Vue.jsやSvelteでの同様のコードの挙動も以下に示しておきます。表中で"表示"としているのは、ボタンクリック直後のUI上での表示内容です。

HTMLのtableバージョン
クリック回数 SolidJS count SolidJS doubled SolidJS textContent SolidJS 表示 React count React doubled React textContent React 表示 Vue count Vue doubled Vue textContent Vue 表示 Svelte count Svelte doubled Svelte textContent Svelte 表示
1 1 2 2 2 0 0 0 2 1 2 0 2 1 2 0 2
2 2 4 4 4 1 2 2 4 2 4 2 4 2 4 2 4
3 3 6 6 6 2 4 4 6 3 6 4 6 3 6 4 6
各実装のPlayground link

この検証はSolidJS作者のRyan Carniato氏によるブログ記事5 Ways SolidJS Differs from Other JS Frameworksで紹介されている内容を元にしています。

[追記] VueとSvelteでのtextContent取得について

上記のVue, Svelteのコード例ではtextContentの値が更新されていない結果になっていましたが、いずれのフレームワークでもDOMの更新を待つためのユーティリティが用意されています。

Vueの場合、nextTickを使ってDOMの更新を待つことで、更新後のtextContentを取得できます。

Vue
<script setup>
import { nextTick } from 'vue'
</script>
...

async () => {
  count++
  await nextTick()
  console.log(`count: ${count}, doubled: ${doubled}, textContent: ${button.textContent}`)
}

Svelteの場合にも同様に、tickを使ってDOMの更新を待つことで、更新後のtextContentを取得できます。

Svelte
<script>
import { tick } from 'svelte'
...
</>

async () => {
  count++
  await tick()
  console.log(`count: ${count}, doubled: ${doubled}, textContent: ${button.textContent}`)
}

追記ここまで

まとめ

SolidJSのFine-Grained Reactivityは、依存関係を動的に管理し、開発者が細かく意識せずとも勝手に最適化されます。また、同期的な状態更新は、直感的でデバッグが容易です。

Reactに慣れ親しんだ開発者にとって、SolidJSは新鮮な驚きを与えるフレームワークだと思います。この記事をきっかけに、ぜひSolidJSを触って、その開発体験の良さを実感していただければ幸いです。

Discussion