React と SolidJS のリアクティブシステムがそれぞれ対照的で面白い。React は冪等性を前提にしていて SolidJS は副作用を前提にしている。真逆だ。詳しく見てみよう。
3行まとめ
- React は冪等で純粋なコンポーネントをベースにしている、副作用はバーチャル DOM の裏側に隠される
- SolidJS ではコンポーネントは1度しか実行されない。その意味で、コンポーネントに冪等性を問う意味はない
- 再実行してほしいコード片を createEffect でマークし、その中で直接副作用を発生さるのが SolidJS 流
リアクティブとは
この記事においてリアクティブシステムとは 「とある変数が更新されたとき、その変数が使われている式や UI が自動的に再計算・再反映される機構」 とします。変数が更新されたらその変数を表示している部分も更新してほしいよね、ということです。
React のリアクティブシステム
React のコンポーネントはこのような姿をしています。
// React function Counter() { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(count => count + 1)}>+1</button> <p>count: {count}</p> </div> ); }
React はコンポーネントが 冪等 であることを期待しています。同じ引数で呼び出す限り、何度呼んでも出力は同じものであると期待するのです。
はい、これには嘘があります。React のコンポーネントは useState を初めとした hooks によって状態を持つことができます。なのでより正確には、同じ引数・同じステートであるかぎり出力は同じであれということです。
ステートと出力については逆方向の期待もあります。コンポーネントの実行に伴ってステートが更新されるべきでは無いというのが逆方向の期待です。
ステートが更新されると React は新しい値を反映させようとします。反映のためにまずコンポーネントを呼び出します。しかしその呼び出しによって再びステートが更新され...。そうやって無限ループが引き起こされてしまいます。
このような無限ループを避けるための要求が 純粋性 です。React コンポーネントは純粋関数で、実行に伴ってステートを更新するようなことがないようにという要求です。ステートが更新されるのはもっぱらユーザー操作によって (より詳細にはイベントハンドラの実行によって) です。
React は冪等で純粋な関数の出力をどうやって DOM に反映させるか
冪等で純粋な関数は扱いやすくて便利です。でも肝心の DOM への反映は誰がどうやってやるのでしょうか?
それをやるのがご存じバーチャル DOM です。更新前後でバーチャル DOM の差分が取られます。その差分こそが現在の DOM に反映させるべきものです。React は最小限の差分だけを反映させます。
このように DOM の更新という名の 副作用 は React エンジンによって裏側に隠されています。
SolidJS のリアクティブシステム
SolidJS のコンポーネントはこのような姿をしています。
// SolidJS function Counter() { const [count, setCount] = createSignal(0); return ( <div> <button onClick={() => setCount(count => count + 1)}>+1</button> <p>count: {count()}</p> </div> ); }
ほとんど React と同じですね。useState が createSignal に置き換わった程度です。ではリアクティブシステムも同じようなものなのか。そうではありません。
一番の違いは コンポーネントが1度しか呼び出されない ことです。1度しか呼ばれないので冪等かどうか考えるまでもありません。さらにバーチャル DOM は登場せず、直接的に実際の DOM に反映されます。
リアクティブのために SolidJS が生成するコード
コンポーネントが1度しか呼び出されないのならどうやって変数の更新を反映させるのでしょうか。それを知るために SolidJS が上記の JSX からどのような JavaScript コードを生成するか見てみましょう。
// <div><button/><p/></div> は下記のようなコードに変換される const div = createElement('div'); const button = createElement('button'); div.appendChild(button); button.textContent = '+1'; button.addEventListener('click', () => setCount(count => count + 1)); const p = createElement('p'); div.appendChild(p); createEffect(() => { p.textContent = `count: ${count()}`; });
div, button, p を作り appendChild している何気ないコードです。
上記のコードの中でリアクティブのために重要なのは createEffect(() => { p.textContent = `count: ${count()}`; });
の行、ここだけです。
createEffect で再実行してほしいコード片をマークする
createEffect は SolidJS における組み込み関数です。名前から React の useEffect みたいなものかな?と思わされますが全然違います。公式ドキュメント には下記のように説明されています。
createEffect は依存する値が更新されるたびに副作用を発生させる一般的な方法です。
値が更新されるたびに createEffect に与えた関数が自動で呼び出されるようにトラッキングします。
大胆に要約すると、createEffect は1度しか呼ばれないコンポーネントの中で 「この部分だけは値が更新されるたびに再実行して!」とマークする ことを可能にします。そして createEffect に渡した関数の中で 直接的に 副作用を発生せるのが SolidJS 流です。さきほどの例では textContent の書き換え p.textContent = `count: ${count()}`;
を発生させていましたね。
<p>{count()}</p>
のような JSX を書くと内部的に createEffect を含むコードが生成されます。それだけでなく createEffect はユーザーが直接的に使うことも可能です。
// SolidJS function Counter() { const [count, setCount] = createSignal(0); createEffect(() => { // count が更新されるたびに localStorage に保存する localStorage.setItem('count', count()); }); return ( <div> <button onClick={() => setCount(count => count + 1)}>+1</button> <p>count: {count()}</p> </div> ); }
createEffect(() => { ... });
を追記して、その中で count の値を localStorage へ保存するようにしました。これにて count が更新されるたび localStorage に保存されるようになりました。
(実はブラウザが更新されたときの localStorage からの読み出しを書いていないのでこのコードは不完全です)
ここまで見てきて「SolidJS はコンポーネントを1度しか実行しない」というのも部分的に嘘であったことがわかりました。 コンポーネント全体は 1度しか呼び出しません。 createEffect でマークしたコード片は 値が更新されるたびに再実行されます。
まとめ: React と SolidJS の対比
React のコンポーネントは冪等かつ純粋であることが期待されています。コンポーネントの中でユーザーが直接的に副作用を発生させることは React 流では無く、副作用 (DOM の更新) はバーチャル DOM の裏側に隠されています。
SolidJS のリアクティブシステムにおいては副作用を発生させる関数こそが主役です。<p>{count()}</p>
のような JSX を書いたときもユーザーが直接的に createEffect を使うときも、内部的には副作用を発生させることが主目的になります。
最後に個人的な感想です。
createEffect の中でユーザー自らが副作用をハンドリングし何度も再実行させるなんて、規模が多くなったときに巨大なスパゲッティになってしまわないか?と、思いながら趣味のプロジェクトで SolidJS を使ってみています。今のところうまく動いているようです。
それにしても React の冪等・純粋な世界観からすると SolidJS の組み込み関数はどれもマジカルな挙動ですね。自明な挙動を好む人は SolidJS は肌に合わないかも。
(え?React hooks もたいがいマジカルだろって?それはそうだね)
私からは以上です。