はじめに
最適なフレームワークを選択できていますか?
WEBアプリケーションを開発する際、現在Reactがデファクトスタンダードになりつつあるのは事実です。
その理由は、Reactが持つ柔軟性や豊富なエコシステムから来る信頼性などが上げられます。
しかし、どんなユースケースにおいても必ずReactが最適な選択とは限りません。
プロジェクトの目的に応じて都度最適なフレームワークを選択する必要があると考えます。
SolidJS
公式サイト
本記事では、WEBアプリを開発する際の新しい選択肢としてSolidJSというフレームワークを紹介します。
SolidJSはReactのような宣言的なUIライブラリでありながら、Reactにはない良さを持っています。
SolidJSの特徴
SolidJSの主な特徴は以下の通りです。
-
Reactのように記述できる
SolidJSは基本的にReactの哲学を持っています。
JSXを使ってコンポーネントを作り、それを積み上げることでアプリケーションを構築します。
Reactなどのフレームワーク経験者にとっては学習コストが低く、すぐに開発を始めることができます。 -
パフォーマンス重視
Reactの仮想DOMとは違い、SolidJSでは状態/プロパティの更新を直接DOMに反映することで、きめ細かいリアクティビティを実現しています。
これにより高頻度に更新があるようなアプリケーションにおいても高いパフォーマンを維持することができます。
SolidJSはUIスピードとメモリ消費において、VanillaのJSとほぼ同等のパフォーマンスを発揮します。(公式調べ)
またバンドルサイズも非常に小さく、アプリケーションのロード時間を短縮することが可能です。 -
シンプルなAPI
SolidJSは比較的シンプルなAPIで構成されており、学習コストが低く始められます。
主要な機能を押さえておけばシンプルなアプリケーションを簡単に構築することができます。
ReactユーザーこそSolidJSを使うべき理由
Reactを使用している開発者にとって、SolidJSへの切り替えは、いくつかの有利な点があります。
-
Reactと記述が似ている
前述したとおり、SolidJSはReactの哲学を受け継いで作られています。
Reactにある便利な機能の多くがSolidJSに標準装備されているため、Reactからの移行が非常にスムーズに行えます。 -
Reactよりも効率的に記述ができる
Reactのようにmemo化による効率化が不要だったり、データバインディングが簡単に記述できるなど、Reactのかゆいところにも手が届くような作りになっています。
SolidJSはReactよりも少ないコードでよりパフォーマンスの良いアプリケーションを開発することができます。
Reactから受け継いでいる良い点
Reactが世界中の開発者から広く支持されている理由は、開発体験の素晴らしさにあります。
SolidJSは、これらの価値ある特性を尊重し、自身の設計に取り入れています。
JSXでの記述
JSXは、JavaScriptのシンタックスを拡張した言語で、XML構文のようにコンポーネントを記述することが可能です。
SolidJSでもReactと同様に、JSXを用いて簡潔なコンポーネントの定義が可能です。
import { render } from 'solid-js/web';
function App() {
return <h1>Hello, Solid!</h1>;
}
render(App, document.getElementById('root'));
TypeScriptのサポート
TypeScriptは、JavaScriptに静的型付けとオブジェクト指向プログラミングの特性を付加した言語で、コードの安全性と生産性を向上させます。
SolidJSもTypeScriptをフルにサポートしており、安全に型を使用しながら開発を行うことが可能です。
import { render } from 'solid-js/web';
interface GreetingProps {
name: string;
}
function Greeting({ name }: GreetingProps) {
return <h1>Hello, {name}!</h1>;
}
render(() => <Greeting name="Solid" />, document.getElementById('root'));
lazy関数
SolidJSはReactのReact.lazy()関数の機能を継承し、遅延ロードやコード分割をサポートしています。
これにより、アプリケーションの初回ロードパフォーマンスを向上させることが可能です。
import { lazy } from 'solid-js';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<div>
<LazyComponent />
</div>
);
}
Portalの利用
ReactのPortalと同じ概念がSolidJSにもあります。
これにより、DOMのツリー構造を超えて子コンポーネントをレンダリングすることが可能となります。
これは、モーダルダイアログなどの特定のコンポーネントを実装する際に非常に便利です。
import { createSignal } from 'solid-js';
import { Portal } from 'solid-js/web';
function App() {
const [isOpen, setIsOpen] = createSignal(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen())}>Toggle Modal</button>
{isOpen() && (
<Portal mount={document.body}>
<div>I am a modal!</div>
</Portal>
)}
</div>
);
}
ErrorBoundaryとSuspense
ReactのErrorBoundaryやSuspenseのようなメカニズムもSolidJSに存在します。
これらを活用することで、エラーハンドリングや非同期処理の待機をうまく管理することができます。
import { createResource } from 'solid-js';
import { ErrorBoundary, Suspense } from 'solid-js/web';
function App() {
const [resource] = createResource(fetchData);
return (
<ErrorBoundary fallback={<div>Something went wrong!</div>}>
<Suspense fallback={<div>Loading...</div>}>
<Component resource={resource} />
</Suspense>
</ErrorBoundary>
);
}
ref属性
SolidJSでは、Reactのref属性と同じように、DOMノードやコンポーネントのインスタンスを参照することができます。
これは、フォームの入力値を取得したり、外部ライブラリとの連携を行う際に非常に便利です。
import { createSignal } from 'solid-js';
function App() {
const [inputRef, setInputRef] = createSignal(null);
return (
<div>
<input ref={setInputRef} />
<button onClick={() => console.log(inputRef().value)}>Log Input Value</button>
</div>
);
}
Reactよりも良い点
Reactから受け継いだ優れた特性と機能に加えて、SolidJSにはReactを上回る強みも多く存在します。
useEffectの第二引数が不要
ReactのuseEffectでは依存性配列(第二引数)を必要としますが、SolidJSではその必要がありません。
これはSolidJSがリアクティブな依存性を自動的に追跡してくれます。
import { createSignal, createEffect, onCleanup } from 'solid-js';
function App() {
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log(count());
});
return <div>Count: {count()}</div>;
}
state/recoilの思想が標準搭載
SolidJSでは、ReactのuseStateやrecoilのような状態管理の仕組みが標準搭載されています。
createSignalはReactでいうところのuseStateと同じようなものですが、コンポーネントの外でも使用することができます。
これにより、recoilのようなグローバルな状態管理を簡単に実現することが可能です。
recoilのようなアトミックな状態管理も、SolidJSのリアルDOMによって実現されています。
import { createSignal } from 'solid-js';
// globalなステート
const [globalCount, setGlobalCount] = createSignal(0);
function App() {
// localなステート
const [count, setCount] = createSignal(0);
return (
<div>
<button onClick={() => setCount(count() + 1)}>Increase</button>
<p>Count: {count()}</p>
<p>Global Count: {globalCount()}</p>
</div>
);
}
classList
SolidJSでは、要素のクラスを動的に操作するためにclassListディレクティブを提供しています。
これにより、コンポーネントのスタイルを状態に基づいて動的に変更することが可能です。
import { createSignal } from 'solid-js';
function App() {
const [isActive, setIsActive] = createSignal(false);
return (
<div
classList={{
active: isActive(),
}}
>
I am {isActive() ? 'active' : 'not active'}
</div>
);
}
カスタムディレクティブ
SolidJSでは、カスタムディレクティブを定義することができます。
これにより、共通の動作を再利用したり、DOMの状態をステートに結びつけたりすることが可能です。
const [name, setName] = createSignal("");
function model(el, value) {
const [field, setField] = value();
createRenderEffect(() => (el.value = field()));
el.addEventListener("input", (e) => setField(e.target.value));
}
<input type="text" use:model={[name, setName]} />;
SolidJSの注意点、React経験者がハマりやすいポイント
SolidJSが提供する優れた特性と機能は魅力的ですが、その一方で注意すべき点やReactユーザーがハマりやすいポイントが存在します。
Signal
SolidJSでは、リアクティブな値の生成や管理に、Signalという概念が使用されます。
このSignalは、状態を保持するための関数で、読み取りと書き込みが可能です。
ただしその振る舞いはReactのuseStateとは若干異なり、これに慣れるまで少し時間がかかるかもしれません。
きちんと理解するには、SolidJSのリアクティビティの仕組みについて理解する必要があります。
Reactでは、ある状態が更新されるとコンポーネントのrender
関数が再実行され、DOMが再レンダリングされます。
再レンダリングの度に仮想DOMが生成され、前回の状態と比較することで差分を検出し、差分のみを実際のDOMに反映しています。
一方SolidJSでは、ある状態が更新されると、その状態に依存するDOM(正確にはDOMを生成する関数)や反応関数(createEffect等)のみが再実行されます。
これにより、同じコンポーネントないの無関係なDOMは影響をうけず、関係するDOMのみが更新されれます。
例えば次のようなコンポーネントがあったときに、button要素をクリックすることでcount
の値が更新されるが、
count
に依存している反応関数と、p要素のtextContent
だけが再実行/再作成されて、無関係のDOMは影響を受けません。
import { createSignal, createEffect } from 'solid-js';
function App() {
const [count, setCount] = createSignal(0);
// 反応関数
createEffect(() => {
console.log(count());
});
return (
<div>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
<p>Count: {count()}</p>
<div>無関係なDOM</div>
</div>
);
}
コードだけみると、useStateと同じように見えますが、実際には異なる振る舞いをしています。
Reactユーザーがやりがちな間違いとして、次のようなコードがあります。
import { createSignal } from 'solid-js';
function App() {
const [count, setCount] = createSignal(0);
// ここ
const color = count() > 0 ? 'red' : 'blue';
return (
<div>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
<p style={{ color: color }}>Count: {count()}</p>
</div>
);
}
このコードでは一見、color
という変数はcount
の値に応じて色を設定しているように見えますが、
この記述方法ではcolor
という変数はcount
の依存関係にあるという風にSolidJSは解釈してくれません。
そのため、count
の値が更新されてもcolor
の値は更新されず、color
の値に応じてp要素の色も更新されません。
このような場合は、color
を関数でラップする必要があります。(もしくはcreateMemoを利用する)
これにより、color
はcount
の値に応じて更新されるようになり、p要素もcolor
の値に応じてstyleが変化します。
import { createSignal } from 'solid-js';
function App() {
const [count, setCount] = createSignal(0);
const color = () => count() > 0 ? 'red' : 'blue';
return (
<div>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
<p style={{ color: color() }}>Count: {count()}</p>
</div>
);
}
Propsの扱い
SolidJSでは、親コンポーネントから子コンポーネントへの値の受け渡し(props)は、Reactと基本的に同じ方法で行われます。
ただし、関数コンポーネントにおけるpropsは、それ自体がリアクティブな値として振る舞います。
そのため、先ほどのSignalと同様に、中身の値を取り出す際は注意が必要です。
import { createSignal } from 'solid-js';
// ng
// countがリアクティブにならない
function Child(props) {
const { count } = props
return <p>Count: {count}</p>;
}
// ok
// propsのまま使う
function Child(props) {
return <p>Count: {props.count}</p>;
}
// ok
// 関数でラップする
function Child(props) {
const count = () => props.count
return <p>Count: {count()}</p>;
}
function App() {
const [count, setCount] = createSignal(0);
return (
<div>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
<Child count={count()} />
</div>
);
}
制御フロー(Show/Switch/For/Index)
SolidJSではリアクティブな値を扱うため、制御フローの記述方法がReactとは異なります。
Show
Reactでは、条件によって表示する要素を切り替える場合、三項演算子を利用することが多いと思います。
function App() {
const [count, setCount] = useState(0);
return (
<div>
{count > 0 ? <p>Count: {count}</p> : <p>Count is 0</p>}
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
</div>
);
}
SolidJSでは、Show
コンポーネントを利用します。
import { Show } from 'solid-js';
function App() {
const [count, setCount] = useState(0);
return (
<div>
<Show when={count() > 0}>
<p>Count: {count()}</p>
</Show>
<Show when={count() === 0}>
<p>Count is 0</p>
</Show>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
</div>
);
}
Switch
Switch
を利用することで、より複雑な条件分岐を記述することができます。
import { Switch, Match } from 'solid-js';
function App() {
const [count, setCount] = useState(0);
return (
<div>
<Switch fallback={<p>Count is {count()}</p>}>
<Match when={count() === 0}>
<p>Count is 0</p>
</Match>
<Match when={count() === 1}>
<p>Count is 1</p>
</Match>
</Switch>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
</div>
);
}
For
Reactでは、配列の要素を展開する場合、map関数を利用することが多いと思います。
function App() {
const [count, setCount] = useState(0);
return (
<div>
{Array.from({ length: count }, (_, i) => (
<p key={i}>Count: {i}</p>
))}
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
</div>
);
}
SolidJSではFor
またはIndex
を使い、配列の要素を展開します。
import { For } from 'solid-js';
function App() {
const [count, setCount] = useState(0);
return (
<div>
<For each={Array.from({ length: count })}>
{(_, i) => (
<p key={i}>Count: {i}</p>
)}
</For>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increase</button>
</div>
);
}
Getting Started
これまでの内容で、SolidJSがReactユーザーに慣れ親しんだ方法で記述できることが分かったかと思います。
またReactユーザーがハマりやすいポイントも押さえたところで、実際にSolidJSを利用してみましょう。
こちらを参考に進めます。
プロジェクトの作成
まずは、SolidJSのプロジェクトを作成します。
TypeScriptを利用して開発することを推奨します。
npx degit solidjs/templates/ts my-app # my-appという名前でプロジェクトを作成
cd my-app # my-appに移動
npm i # 依存関係のインストール
npm run dev # ブラウザが立ち上がる
ファイル構成
SolidJSのプロジェクトは、Reactのプロジェクトとほぼ同じ構成で開発ができます。
Reactにも流派があるように、正解はないですが私がよく採用する構成を紹介します。
my-app
├── package.json
├── src
│ ├── assets
│ │ └── favicon.svg
│ ├── components/ # コンポーネント
│ ├── utils/ # ユーティリティ
│ ├── pages/ # ページ
│ ├── App.tsx
│ ├── index.css
│ └── index.tsx
├── index.html
├── tsconfig.json
└── vite.config.ts
ルーティング
App.tsxにルーティングを記述します。
ルーティングには@solidjs/router
というライブラリを利用します。
次のように記述することで、ルーティングを実現できます。
import { Route, Routes } from '@solidjs/router';
import { Component, lazy } from 'solid-js';
const App: Component = () => {
return (
<Routes>
<Route path="/" component={lazy(() => import('./pages/Home'))} />
</Routes>
);
};
export default App;
チュートリアル・サンプルコード
チュートリアルやサンプルコードは次を参照すると良いです。
https://www.solidjs.com/tutorial/introduction_basics
https://www.solidjs.com/examples/counter
SolidJSのユースケース
SolidJSは、Reactと比較してまだまだ利用者が少なく、Reactの大きなコミュニティと豊富なエコシステムと比較するとデメリットが多いです。
一方で、SolidJSがパフォーマンスを発揮するようなユースケースも存在します。
高頻度で状態が更新されるようなアプリケーション
例えばマウスの座標を状態に記録し、その座標を扱うようなアプリケーションがある場合、
ReactだとuseState
を利用して状態を管理することになります。
Reactでは、状態が更新されると再レンダリングが走るので、ミリ秒単位で再レンダリングが行われることになります。
これだけだとそこまで影響はないように見えますが、アプリケーションが複雑化するにつれて再レンダリングのコストが上がっていき、画面がカクつくようになります。
import { useState } from 'react';
function App() {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<div>
<p>Position: {position.x}, {position.y}</p>
<div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
<p>Move here</p>
</div>
</div>
);
}
SolidJSでは、createSignal
を利用して状態を管理することができます。
SolidJSでも同様にミリ秒単位で状態の更新が行われますが、再実行されるのはposition
に依存する関数のみなので影響範囲は最小限に押さえることができます。
Reactだとカクつきが気になるようなアプリケーションでも、SolidJSで実装することでカクつきが気にならなくなります。
import { createSignal } from 'solid-js';
function App() {
const [position, setPosition] = createSignal({ x: 0, y: 0 });
return (
<div>
<p>Position: {position().x}, {position().y}</p>
<div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
<p>Move here</p>
</div>
</div>
);
}
フォームをよく使うアプリケーション
フォームをよく使うアプリケーションでは、フォームの入力値を状態として管理することになります。
Reactでフォームを使う場合、inputタグのvalue属性に状態をバインドすることでフォームの入力値を状態として管理することができます。
しかし、Reactでは状態が更新されると再レンダリングが走るので、フォームの入力値が更新されるたびに再レンダリングが走ってしまいます。
SolidJSでは状態が更新されても全体の再レンダリングは走らないので、スムーズなフォームを実装することができます。
それでもReactを採用するケース
中規模以上のアプリケーション
やはりReactの豊富なエコシステムはとても優秀です。
SolidJSはまだまだ利用者が少なく、エコシステムもまだまだ整っていません。
アプリケーションの規模が大きくなるにつれて、安定して動く周辺ライブラリがある方が便利です。
SolidJSでは周辺ライブラリの選択肢が限定的で、機能も不十分なことが多いです。
このように中規模以上のアプリケーションにおいてはReactを選択する方が無難な場合もあります。
React-to-SolidJS
それでもSolidJSを選択してくれる読者に、React脳からSolidJS脳になるためのTipsをいくつか紹介します。
useXX to createXX
ReactではuseXX
という名前のフックを利用して状態を管理します。
SolidJSではcreateXX
という名前の関数を利用して状態を管理します。
主要な対応表は以下です
useState/atom(recoil) = createSignal
useStateはローカルの状態、atomはグローバルの状態を管理するのに使うと思います。SolidJSではcreateSignal
をローカル・グローバル問わず利用することができます。
useEffect = createEffect
状態の変更に応じて、何かを実行したい際はcreateEffect
を利用します。
useEffectと違い、コンポーネントの最初に記述しないといけないというルールはありません。
コンポーネントの外でも定義することができます。
useEffectと違い第二引数も必要なく、自動的にどの状態に依存しているかを判断してくれます。
createMemo
も同様にコンポーネントの外から利用可能で、第二引数も不要です。
styleはcamelCaseではなくkebab-case
// React
<div style={{ backgroundColor: 'red' }} />
// SolidJS
<div style={{ 'background-color': 'red' }} />
最適化のコツ・上級者になるには
SolidJSは極めようと思うとかなり奥が深いです。
最適化や上級者になるためのコツをほんの一部紹介します。
batchによる一括更新
ある処理の中で複数のSignalを更新する場合、それぞれのSignalの更新によって依存関係にあるDOMや反応関数が再実行されます。
これを一度の更新にまとめることで、再実行される回数を減らすことができます。
import { batch } from 'solid-js';
function App() {
const [count, setCount] = createSignal(0);
const [count2, setCount2] = createSignal(0);
const sum = createMemo(() => count() + count2())
const handleClick = () => {
batch(() => {
setCount(count() + 1);
setCount2(count2() + 1);
});
};
return (
<div>
<p>Sum: {sum()}</p>
<button onClick={handleClick}>Click</button>
</div>
);
}
For/Indexの使い分け
配列をDOMに展開する方法として、For
とIndex
があります。
正しく使い分けることがパフォーマンスに大きく影響します。
どちらを使うのが適切か、Reactユーザーにとっては非常に分かりやすい見分け方があります。
Reactで配列を操作する際は配列のmapメソッドを利用します。
展開した要素には必ずkey
を設定しないといけません。
このkey
に要素のキーとなるようなIDを使う場合は、For
を使ってください。
key
に要素のインデックスを使う場合は、Index
を使ってください。
// 配列のmapメソッドを利用する場合
const arr = ['a', 'b', 'c'];
// Reactでkeyに要素のキーとなるようなIDを使う場合
arr.map((item) => <div key={item}>{item}</div>);
// SolidJSではForを使う(indexがSignal)
<For each={arr}>{(item, index) => <div>{index()}. {item}</div>}</For>
// Reactでkeyに要素のインデックスを使う場合
arr.map((item, index) => <div key={index}>{item}</div>);
// SolidJSではIndexを使う(itemがSignal)
<For each={arr}>{(item, index) => <div>{index}. {item()}</div>}</For>
For
を使う場合にも注意点があります。
例えばユニークな文字列の配列をDOMに展開する場合は、そのままFor
を使うことで最適化されます。
例えばこの配列のSignalが更新された場合、更新された要素のみが再描画されます。
// ユニークな文字列の配列
const arr = ['a', 'b', 'c'];
// これは最適化される
<For each={arr}>{(item) => <div>{item}</div>}</For>
一方でオブジェクトの配列を扱う場合、Reactユーザーからすると違和感を覚える箇所があります。
SolidJSにはkey
を指定する場所がありません。
オブジェクトをFor
で展開する場合、そのオブジェクトが更新をされると配列全体が再描画されることになります。
これを防ぐために、オブジェクトの配列をFor
で扱う際には一度、キーとなるプロパティを使って文字列の配列に変換します。
そのうえで、キーとなるプロパティからオブジェクトを取得できるようなMapを作成します。
この二つを使い、For
で展開することで最適化されます。
少し回りくどいですが、この方法でないと配列全体が再描画されてしまうので注意してください。
import { createSignal, createMemo, For } from "solid-js";
interface Item {
id: string;
name: string;
}
// オブジェクトの配列
const [arr, setArr] = createSignal<Item[]>([
{ id: "a", name: "A" },
{ id: "b", name: "B" },
{ id: "c", name: "C" },
]);
// キーの配列に変換
const keys = createMemo(() => arr().map((item) => item.id));
// Mapを作成
const map = createMemo(() => {
const map = new Map<string, Item>();
arr().forEach((item) => map.set(item.id, item));
return map;
});
function App() {
return (
<For each={keys()}>
{(key) => {
const item = () => map().get(key)!;
return <div>{item().name}</div>;
}}
</For>
);
}
最後に
SolidJSは完成度が高いですが、エコシステムやコミュニティはまだまだ発展途上のフレームワークです。
ユーザーが増えることでエコシステムも整っていくことが見込めます。
ぜひ一度SolidJSを触って、一緒にSolidJSを盛り上げましょう!