SolidJSの開発体験の良さを知ってほしい
はじめに
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更新)を再実行します。依存配列を手動で指定する必要はありません。
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での例
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で実装してみましょう。
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 |
この検証はSolidJS作者のRyan Carniato氏によるブログ記事5 Ways SolidJS Differs from Other JS Frameworksで紹介されている内容を元にしています。
textContent
取得について
[追記] VueとSvelteでの上記のVue, Svelteのコード例ではtextContent
の値が更新されていない結果になっていましたが、いずれのフレームワークでもDOMの更新を待つためのユーティリティが用意されています。
Vueの場合、nextTick
を使ってDOMの更新を待つことで、更新後のtextContent
を取得できます。
<script setup>
import { nextTick } from 'vue'
</script>
...
async () => {
count++
await nextTick()
console.log(`count: ${count}, doubled: ${doubled}, textContent: ${button.textContent}`)
}
Svelteの場合にも同様に、tick
を使ってDOMの更新を待つことで、更新後のtextContent
を取得できます。
<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