最近 Solid JS というフロントエンド・ライブライリーが話題になっているので調べてみました。
Solid JSのホームページ(日本語ページもあります😊)には
- 高いパフォーマンス
- パワフル
- 実用本位
- 生産的
と書かれていますが、ホームページを読んだり簡単なコードを書いて私がSolidに感じたのは
- 超高速
- Reactライク
です、これから説明していきます。
超高速らしい
ホームページにはいろいろなフロントエンド・ライブライリーとの速度比較(ベンチマーク)結果のグラフが掲載されています。以下がその画像です。
1番上のVanillaはライブラリー等は使わずにJavasciptと標準のAPIのみを使って書いたコードで、この速度を基準(1.0)にしています。
まず下の方を見るとVueは1.54、Reactは1.93と出ています、まあそんなものかなと思います。
Reactでササッと書いたコードを、手動でDOM APIだけを使って書くのは結構たいへんだと思います。頑張って書き換えれば2倍ちかい性能になるということですね。頑張っても2倍にしかならないとも言えます。
そこから上にあがって行くと以前取り上げたPreactやSvelteが出てきます。Preactは軽量なReact互換ライブラリーです、またSvelteは仮想DOMを使わずにDOMを書き換えるコードを自動生成するライブライリーです。
InfernoはSolid同様にReactライクで高速性をうたっているライブラリーです。 地獄(Inferno)とは凄い名前ですね😲 Infernoのホームページにもベンチマーク結果ページがあり興味深いですね。
そして、2番のSolid JSは1.05でほぼVanillaに接近する性能をほこっています。性能に付いての考察は次回のブログで検討していみたいと思います。ホームページによるとSolidはSvelte同様にデータ等の変更に対応してDOMを書き換えるコードを自動生成するそうです。
Reactライクなライブラリー
さて、Solidのもう1つの特徴はReactにとても近いということでしょうか。次のコードはUPボタンを押すとカウンターが増えて行くReactのコードです。
React
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>up</button>
<div>Count: {count}</div>
</>
);
};
export default App;
そして、同じ機能をSolidで書いたものが下になります。遠目で眺めると同じように見えます。
- コンポーネントは関数です
- HTML生成にはJSXが使われています
- ReactのState管理useState関数と似ている、createSignal関数でSiginal(時間の経過とともに変化する値)を管理しています
- State/Signalの更新はどちらも
setCount()
関数です
Solid
import { createSignal } from "solid-js";
const App = () => {
const [count, setCount] = createSignal(0);
return (
<>
<button onClick={() => setCount(count() + 1)}>up</button>
<div>Count: {count()}</div>
</>
);
};
export default App;
しかし注意深く見ると、Signal値の参照はcount()
関数呼び出しです。Reactではcount
は変数(定数)ですよね。
ReactではStateの値が変わるとコンポーネント(関数)が再描画(再実行)されますが、Solidでは再描画(再実行)は行われずにcount()
対応するDOM(HTML)のみ書き換えられます。
Reactでも仮想DOMがあるので画面の書き換えはcount
の部分のみななりますが、仮想DOMを作ったり変更分を検出する処理は再描画の度に実行されます。それがないSolidの高速性になりそうですね。
いつものジャンケンのコードをSolidで書いてみた
Vue3に入門してみた(TypeScriptを使ったReactとVue3の比較)、Svelteに入門してみた(React/Vueとの違い)などで使ったジャンケンアプリをSolidで書いてみました。
1. メイン
まずはメインのコンポーネント。上で説明したように遠目に見るとほぼ同じです。
- useState → createSignal に変更、当然インポート先も変更
React
import React, { useState } from 'react'
import JyankenBox, { Te } from './JyankenBox'
import ScoreBox, { ScoreType, Jjudgment } from './ScoreBox'
const Jyanken = () => {
const [scores, setScrores] = useState<ScoreType[]>([])
const pon = (human: Te) => {
const computer: Te = Math.floor(Math.random() * 3)
const judgment: Jjudgment = (computer - human + 3) % 3
const score = {human: human, computer: computer, judgment: judgment}
setScrores([score, ...scores])
}
return (
<>
<h1>じゃんけん ポン!</h1>
<JyankenBox actionPon={te => pon(te)} />
<ScoreBox scores={scores} />
</>
)
}
export default Jyanken
Solid
import { createSignal } from 'solid-js'
import JyankenBox, { Te } from './JyankenBox'
import ScoreBox, { ScoreType, Jjudgment } from './ScoreBox'
const Jyanken = () => {
const [scores, setScrores] = createSignal<ScoreType[]>([])
const pon = (human: Te) => {
const computer: Te = Math.floor(Math.random() * 3)
const judgment: Jjudgment = (computer - human + 3) % 3
const score = {human: human, computer: computer, judgment: judgment}
setScrores([score, ...scores()])
}
return (
<>
<h1>じゃんけん ポン!</h1>
<JyankenBox actionPon={te => pon(te)} />
<ScoreBox scores={scores} />
</>
)
}
export default Jyanken
2. ボタン
グー・チョキ・パーのボタンが並ぶコンポーネントも、遠目に見るとほぼ同じです。
ただし、良く見るとstyleの指定が "margin: 0 10px; padding: 3px 10px; fontSize: 14"
のように文字列で指定できます。styled-componentsをインストールしなくても従来のHTMLと同様に書けます(React同様にオブジェクトで指定できます)。
React
import React from 'react'
export enum Te { Guu = 0, Choki, Paa}
type JyankenBoxProps = {
actionPon: (te: number) => void
}
const JyankenBox: React.FC<JyankenBoxProps> = ({actionPon}) => {
const divStyle: React.CSSProperties = {margin: "0 20px"}
const buttonStyle: React.CSSProperties = {margin: "0 10px", padding: "3px 10px",
fontSize: 14}
return (
<div style={divStyle}>
<button onClick={() => actionPon(Te.Guu)} style={buttonStyle}>グー</button>
<button onClick={() => actionPon(Te.Choki)} style={buttonStyle}>チョキ</button>
<button onClick={() => actionPon(Te.Paa)} style={buttonStyle}>パー</button>
</div>
)
}
export default JyankenBox
Solid
import { Component } from 'solid-js'
export enum Te { Guu = 0, Choki, Paa}
type JyankenBoxProps = {
actionPon: (te: number) => void
}
const JyankenBox: Component<JyankenBoxProps> = ({actionPon}) => {
const divStyle = "margin: 3px 20px"
const buttonStyle = "margin: 0 10px; padding: 3px 10px; fontSize: 14"
return (
<div style={divStyle}>
<button onClick={() => actionPon(Te.Guu)} style={buttonStyle}>グー</button>
<button onClick={() => actionPon(Te.Choki)} style={buttonStyle}>チョキ</button>
<button onClick={() => actionPon(Te.Paa)} style={buttonStyle}>パー</button>
</div>
)
}
export default JyankenBox
3. 結果表示
対戦結果の表を表示するコンポーネントは、遠目にみても少し違いが見えます。
表の行を表示する繰り返しは、map関数ではなく<For>
という専用のタグを導入しています。ドキュメントによるとmap関数でも表示できるようですが、非効率だそうです。
<For>
以外にも条件で表示を切り替える<Switch>
などの制御フロー用タグが用意されています。 従来のテンプレート言語に近いですね。
React
import React from 'react'
export enum Jjudgment { Draw = 0, Win, Lose }
export type ScoreType = {
human: number;
computer: number;
judgment: Jjudgment;
}
type ScoreListProps = {
scores: ScoreType[]
}
const ScoreBox: React.FC<ScoreListProps> = ({scores}) => {
const teString = ["グー","チョキ", "パー"]
const judgmentString = ["引き分け","勝ち", "負け"]
const tableStyle: React.CSSProperties = {marginTop: 20, borderCollapse: "collapse"}
const thStyle: React.CSSProperties = {border: "solid 1px #888", padding: "3px 15px"}
const tdStyle: React.CSSProperties = {border: "solid 1px #888", padding: "3px 15px",
textAlign: "center"}
return (
<table style={tableStyle}>
<thead>
<tr>
<th style={thStyle}>あなた</th>
<th style={thStyle}>コンピュター</th>
<th style={thStyle}>勝敗</th>
</tr>
</thead>
<tbody>
{scores.map((scrore, ix) =>
<tr key={ix}>
<td style={tdStyle}>{teString[scrore.human]}</td>
<td style={tdStyle}>{teString[scrore.computer]}</td>
<td style={tdStyle}>{judgmentString[scrore.judgment]}</td>
</tr>
)}
</tbody>
</table>
)
}
export default ScoreBox
Solid
import { Accessor, Component, For } from 'solid-js'
export enum Jjudgment { Draw = 0, Win, Lose }
export type ScoreType = {
human: number;
computer: number;
judgment: Jjudgment;
}
type ScoreListProps = {
scores: Accessor<ScoreType[]>
}
const ScoreBox: Component<ScoreListProps> = ({scores}) => {
const teString = ["グー","チョキ", "パー"]
const judgmentString = ["引き分け","勝ち", "負け"]
const tableStyle = "margin-top: 20; border-collapse: collapse"
const thStyle = "border: solid 1px #888; padding: 3px 15px"
const tdStyle = "border: solid 1px #888; padding: 3px 15px; text-align: center"
console.log("scores", scores);
return (
<table style={tableStyle}>
<thead>
<tr>
<th style={thStyle}>あなた</th>
<th style={thStyle}>コンピュター</th>
<th style={thStyle}>勝敗</th>
</tr>
</thead>
<tbody>
<For each={scores()}>{
scrore => (
<tr>
<td style={tdStyle}>{teString[scrore.human]}</td>
<td style={tdStyle}>{teString[scrore.computer]}</td>
<td style={tdStyle}>{judgmentString[scrore.judgment]}</td>
</tr>
)}
</For>
</tbody>
</table>
)
}
export default ScoreBox
動かなかったコード
最初Reactから単純に置き換えたら、結果表示がまったく行われませんでした。その時のコードは以下のようになっていました。
- メインでは対戦結果の入った配列の取得関数を実行し、値をScoreBox(結果表示)コンポーネントに渡していました
- ScoreBoxコンポーネントでは受け取った
scores
配列を<For>
タグに渡していました
メイン
return (
<>
<h1>じゃんけん ポン!</h1>
<JyankenBox actionPon={te => pon(te)} />
<ScoreBox scores={scores()} />
</>
)
結果表示
type ScoreListProps = {
scores: ScoreType[]
}
const ScoreBox: React.FC<ScoreListProps> = ({scores}) => {
・・・
<tbody>
<For each={scores}>{
SolidはReactのように引数(props)が変更されても再描画(関数の再実行)を行わないので、このコードでは結果は表示されません。
正しくはScoreBoxコンポーネントに取得関数を渡し、ScoreBoxコンポーネント内で取得関数を実行するように <For each={scores()}>{
と書くことで結果が表示できるようになりました。
まとめ
Solid JSの特徴は
- 超高速
- Reactライク
です。
ReactライクはReact互換ではありません、Reactのように再描画が起きないことはコードの書き方に影響を与えます。そこでreact2solidというReactからSolidに変換するツールも開発されいるようです。😊
また、Solidでテンプレート言語のように制御タグが導入されています。さらにVueのように標準の状態管理ストアなども用意されています。
よくReactはSimpleだがEasyではないと言われますが、SolidはEasyの方にも舵をきっているように思えます。
それから、Preact, Inferno, SolidなどのReactライクなライブラリーが複数あることを知りました。もしReactが無くなってしまっても安心ですね。😅