SolidJS いいよ!
気になった@uhyoさんによるランキング記事。
React脳と自称されてますし、個人的ランキングはそれで良いと思います。
しかしながら、SolidJSの良くないとされたサンプルはReactじゃないんだからさすがに動かないのが当然では?と直感で思ったものだったので、なぜ動かないかをまとめてみました。
筆者について
大昔はjQueryでSPAサイトを作り、Angularを3,4年ほど使い、転職をきっかけにここ1年くらいReactを使ったところ、Reactのエコシステムの素晴らしさを実感しつつも、生のJS(特にイベント関連)との相性の悪さに嫌気がさしてSolidJSを触りだした("SolidJS 完全に理解した")ところ
何がマズイか
以下コードは元記事からの引用。
function Counter() {
const [count, setCount] = createSignal(0);
setInterval(() => setCount(count() + 1), 1000);
const c = count(); // count()の呼び出しをreturn文の外に出したら動かなくなった!
return <div>Count: {c}</div>;
}
Reactと比べたとき、SolidJSの大きな特徴は コンポネント関数を初期化の一度しか呼ばれない ことで、上記コードもそれを理解された上で書かれています。
(そうでなければ、setIntervalをComponent直下に書かない。ReactならuseEffect() で包むなどしなければ、レンダリングごとにインターバルが増えてタイマー爆発が起こるところ。)
一度しか呼ばれないのはreturnに含まれないcount()
も同じです。count()
の戻り値はプリミティブなnumberなので、参照を外部更新するなどもできません。
SolidJSの監視/変更対象はコンポネント関数の戻り値に含まれたコールパスだけ
たとえば以下のようなパターンは正常にカウントアップします。
// count()を関数でラップ
const Counter: Component = () => {
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
const callCount = () => {
return count();
}
return (
<div>
{callCount()} : {Math.random()}
</div >
);
};
---
// JSXを関数でくるむ
const TestComponent: Component = () => {
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
return () => (
<div>
{count()} : {Math.random()}
</div >
)
};
---
// 予めJSXを変数に格納した場合
const TestComponent: Component = () => {
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
const result = (<div>{count()} : {Math.random()}</div>);
console.log("test");
return result;
};
逆にuseEffectに近い動きをするcreateEffectを使っても、以下のようなJSXは更新されません。
const TestComponent: Component = () => {
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
let c = 0;
createEffect(() => {
// 1秒ごとにこの無名関数は呼ばれ c は更新できログもカウントアップ出力されるが、
// Component()が再度実行されるわけではないのでJSX内のメッセージは変わらない
c = count();
console.log(c);
})
return () => (
<div>
{c} : {Math.random()}
</div >
)
};
JSXの実態
JSXの内容をconsole.log()で吐き出すと以下のような感じ
console.log(() => (<SubComponent>{count()} : {Math.random()}</SubComponent>))
() => _$createComponent(SubComponent, {
get children() {
return [_$memo(() => count()), " : ", _$memo(() => Math.random())];
}
})
ここのcreateComponentは、SolidJSではreadSignal
という関数を返します。JSXはDOMの階層構造を関数の呼び出しスタック構造で表現しているにすぎません。そこはReactも同じです。
SolidJSは最初のレンダリング過程のcount()
が呼ばれるタイミングで参照元コンポネントが持つobserverにイベントリスナーを登録します。
{}
は_$memo(()=>count())
とラップされているので、メモ化されたオブジェクトと変更イベントに応じてコールバックを呼び出すか否か判断しchildrenを返している流れ(だと思います)。
所感
長くなりましたが、JSXの<Tag />
実態が関数であり、レンダリング時に実行されていること、裏側で必要に応じて再レンダリングされていることを理解していれば、最初の例が動かないのはJavaScriptの動きとしては自明で、それほど不自然な記法と思いません。
Reactはユーザが書いたコードも全部走らせるので素直といえば素直ですが、大規模になるとレンダリングが走るタイミングが読みづらいのが個人的に辛いと感じるポイントです。
まとめ
SolidJSはレンダラー関数が1度しか呼ばれないことで、setInterval()はじめJSネイティブの機能を書きやすいのは非常にありがたいです。
一方でSolidJSもpropsの扱いなどやや特殊なところもあり、Reactと似てるからこその初見殺しな部分は色々ありますが、ReactHooksの挙動も初見には理解に苦しむところもあるので、結局慣れの問題ではというところ。
createStore()でReduxやRecoilのようなstore実装が簡単かつデフォルトで備えている点も良いと思います。
あとはReact並みにバリエーション豊かなエコシステムが来てくれると本格採用できるのですが、そこまではまだ時間がかかりそうですね。ライブラリが育ってくれることを期待しています。