はじめに
周りの迷惑も顧みずに長々と続けてきたこの連載もいよいよ終わりが近づいてきました。
おそらくこのネタを引っ張るのは限界でしょうし、この連載中にもいくつか示したように他に書きたいテーマがありますので。
前回、「次回は useTransition の解説です」と書いてしまいましたがその約束を果たせそうにもありません。
ただ、その過程において知見を得ましたので、その知見を共有して、謝罪に変えたいと思います。最後までお付き合いいただい方(いないでしょうけど)、誠に申し訳ありませんでした。
謝罪にいたる経緯について(いいわけ)
Async 機能の一環として Suspense を実現したのであれば、かの useTransition
も実現したくなるのが親心というものです。私は Gemini 上級 SE の助けを借りながら一生懸命努力しました。
しかし、私には荷が重すぎました。
Svelte において useTransition の特徴を実現することの難しさ
useTransition は別世界の出来事と捉えよ、という解説(公式だったでしょうか?)を読んだ記憶があるのですが、世界観より実動作を重視してみますと、useTransition は過去の描画を保持したままで次に描画すべきリソース取得を開始するという点に特徴があると考えています。逆にいえば、その特徴がないのであれば Suspense だけで十分であり存在価値がありません。
過去の描画を保持する、というのは結局のところ、仮想 DOM とまではいかなくても、過去の描画すべき情報(以下「過去情報」)を保持しておくということになります。
一方、Svelte のリアクティブ変数を使うとその変数に変更がある度に再描画となります。
リアクティブ変数とそうでない変数をきちんと使い分けないと、どんどん再描画要請が来てしまうわけですが、それの使い分けが大変難しく、そして、再描画時に過去情報を与えるのか現情報を与えるのかを区別して処理することが、私の力量ではとてもついていけるものではありませんでした。
特に難しかったのが、リソースの取得処理中に、新たに fetcher が起動された場合、古い取得処理による再描画をしないようにしないと、ちらつきが出てしまうという点です。古いリソース表示⇒古いリソース表示⇒やっと最後のリソース表示みたいなちらつきだらけのもので満足できるはずがありません。
なんとなく実現できそうな気配はあったのですが、ソースが複雑怪奇なものとならざるを得ず、こんなことをするなら Svelte の描画処理から独立した仮想 DOM を作った方が簡単なのではないかと思いはじめましたのでミッションアボートとなりました。 (^^;
描画のちらつきを抑えるために仮想画面を導入する・・・それは、特にゲームプログラミングを行う際には初心者プログラマでもよく知っていることです。
SolidJS にも useTransition はあると思うのですがどうやって実現しているのでしょうか。やはり仮想 DOM 的なものとはいえないまでも近いものを使って過去情報を保持していているとしたら、React の仮想 DOM 形式もまんざらでない気がしました。そのことを認識できただけでもやって後悔はしておりません。
JavaScript の Microtask の壁
「NodeJS 製の Web サーバーはアクセスが集中しても落ちにくい」のような事実を聞いたことがあると思いますが、それは JS がシングルタスクであるため、アクセスが集中してもリソースの消費がさほど増加しないからです。逆にいうと、サイトを訪れたお客様をお待たせすることで安定性を確保しているといえます。
JS はシングルタスク、の意味は、JS の Async は OS のスレッドのような本当のマルチタスクではないという意味です。そのため、処理のカタマリの途中で別の処理が割り込んでくるようなことはありませんが、あくまでここでの話は JS 上での話で、OS のスレッド処理すら割り込んでこれないとかそう意味ではありませんので混同しないようにしてください。
例えば次のような場合、
let share
async function fn1() {
share = 1
fn2()
console.log(share)
}
async function fn2() {
share = 2
}
コンソールには 絶対に 1
と表示されます。それは、fn2
は Promise を返し、その Promise の実行は fn1
の実行が完了した後(実行完了直後とは限りません)に実行するようにスケジューリングされるだけで、絶対に 他の処理が割り込むことはないからです。
これが理解できない方は、このまま先に進んでも何も理解できないと思いますので上級 SE にハドって一緒に読み進めてください。
一方、次のように await fn2()
とした場合は、
let share
async function fn1() {
share = 1
await fn2()
console.log(share)
}
async function fn2() {
share = 2
}
コンソールには 絶対に 2
と表示されます 。それはシングルタスク故、必ず await
により fn2
の実行が終了した後に console.log(share)
が実行されるからです。
ここまで理解できている方は先に進みましょう!
この例のように、fn2
がやっていることが明確ならこの理解で何の問題もない(なので fetch
などごく一般的な処理や、内容を十分把握している自作関数に await
を仕掛けても問題は起こりえない)のですが、fn2
が React や Svelte のような複雑な処理を行うライブラリが用意している関数である場合ですと、人間的には理解できないような(僕だけだったらごめんなさい)順番でタスクがスケジューリングされることがあります。
それが、「Microtask の壁」です。
私の拙文では理解できない可能性が高いため、上級 SE とハドりながら読むことをおすすめします。
JS で Microtask を作成する方法は 4 つありますが、ここでは次の3つだけが問題になります。
(p) Promise().resolve().then()
(q) queueMicrotask()
(a) await fn()
そして、悪いことに我々作業員が一番使いやすい (a) は、(p), (q) よりも後回しにされることがあるのです。
React や Svelete の中の人はそれを重々承知していますので、あえて (p) や (q) を使います(だからこそこの問題が生じるともいえますが)。
我々も原始に帰り、(p) や (q) だけ使えばいいのですがはっきりいってそれは無理な注文というものです。
もちろん仕様通りの動作なので文句を言っても始まらないわけですが「そこまで知ってないとプログラミングできないんかーい!」となるのが私を含めた普通の作業員のキモチのはずです。「そもそもあんたの書いたアルゴリズムがなんかキモイ」と言われれば反論の余地もないわけです。まあ私の場合は Gemini 上級 SE もお手上げ状態だったので「AIすら混乱するのだ。私は悪くない」と開き直ってますが。
・・・ということで、私の長い旅は終了となりました。期待していた方(いないと思いますが)本当にごめんなさい。
Microtask 問題と React の pure ルールの関係
React でも Microtask 問題は生じうるわけですが、React では、pure ルールを遵守していれば、我々に あんな子細な Microtask についての知識がなくてもよいようになっています。すなわち、React の pure ルールは Microtask 問題を回避するためにある、といっても過言ではありません。もしかしたら、鋭意作成中といわれる伝説の React Compiler(以下 rc) のためでもあるのかもしれませんが、私は「rc は React の仕組みをよく知らずに useMemo
や useCallback
を多用してしまう人専用の機能」ぐらいにしか思っていませんので(今のうちに謝罪します。多分私の勘違いです)、私には関係ありません。私は待望の use
が搭載された React 19 で十分満足しております。もうすぐでるらしい Activity(=Offscreen?)
も便利そうですが、それよりも Jotai
に準ずる機能を標準搭載してもらった方がはるかにうれしいです(もしかしたら 公式 Blog が研究中と記していた const value = use(store);
がそれ?)。
Svelte においても、リアクティブ変数を「普通に」使っている分にはこの子細すぎる Microtask 問題を気にする必要はないのだと思います。ただ、私のように無理やり Async しようとするとそれを理解していないと詰む、という感じでしょうか。
いずれによ、私に火があることは間違いないです。
pure ルールについての更なる具体化?
この連載中に少し触れましたが、React で pure ルールが求められるのは Main Effect すなわち仮想 DOM に関する処理の部分であり、Side Effect でまで pure ルールが求められるものではありません。
既にお伝えした通り「私はどこまでも pure でいたい!」というのは個人の自由、すなわち多様性の尊重に他なりませんので、誰にもとめる権利はありません。
Svelte においては pure ルールのような表現はありませんが、リアクティブ変数が描画処理を司ることを考えれば、それが React の仮想 DOM にあたるものといえそうです。すなわち、仕組み的には全然違うのでしょうが、我々普通の作業員から見れば「仮想DOM?リアクティブ変数?どっちもどっち」でいいんだと思います。
そして、Svelte においても「Side Effect は $effect
など定められたところでしかやっちゃダメ」というルールがあるので、結局それが Svelte における Pure ルールの表現なのではないかと思った次第であります。
なお、モジュールが import 直後に一度だけ実行されるモジュールの実行部分(Svelete でいうと <script module>
の部分)、あそこで Side Effect を実行するのは問題ないはずです。なぜならコンポーネントが起動する前であり、奴らの管轄外だからです。もちろん、「俺は自由だ〜」と、setTimer などの遅延実行を駆使して、仮想 DOM やリアクティブ変数に後で影響を与えるような小細工をするのは絶対にだめですよ。それはもはや Side Effect ではなく Main Effect です。
Svelte の <script>
にあたる部分(React でいえばクラス型コンポーネントのコンストラクタ部分)で Side Effect を実行するのはダメです。そこはもはや奴らの管轄に他ならないからです。なお、React の関数型コンポーネントにはそれにあたる部分はありません(ないはず)。関数型コンポーネント中心になる前に一時盛り上がった話題ですが React の中の人は「んなものなくてもよい」とあえて判断して今に至っております。
一番はっきりしているのは、React のクラス型コンポーネントの render
メソッドで Side Effect を実行するのは絶対にダメです。厳密にいえば console.log()
を実行するのすらダメということになりますが、さんざんデバッグ用に仕掛けてきましたがそれでおかしくなったことはありません。でもダメなものはダメなのです。はみ出してはいけません。それがルールというものです。
クラス型コンポーネントの render
メソッドにあたるものが関数型コンポーネントの本体にあたるわけですから、そこで Side Effect を実行するのも禁止です。ただここが少しわかりにくいのですが useEffect
Hook を使えば可能です。その点については下記に少し解説しておきました。
Svelte で、React のクラス型コンポーネントにおける render
メソッドにあたるものはどこなのでしょうか。ないのかな?ないから pure ルール的なものがないのかもしれません。いずれにせよ Svelte で、 Side Effect の実行が許されるのは $effect
を始めとする $xxxxxx
関数のうち「ここならええよ」と公式に明記があるところでだけです。
onclick
などのイベント処理部分で Side Effect を実行できることには一点の曇りもないと思います。
せっかく古のクラス型コンポーネントの話題が出たのでついでですが、「React の Hook むつかしすぎる! Hxxxxxxk!」という声をたまに見聞きします。そのような方は一度クラス型コンポーネントについて調べてみることをお薦めします。クラス型コンポーネントには props
や state
や、内部的な情報を保持するメンバ、さらにはイベントを実行するメソッドなどを好き勝手に追加できます。その副作用(日本語のニュアンス通りの副作用)として bind
やらなにやらめんどくさい処理が必要になる点が嫌われ廃れていきました。おそらく JS のクラスが他の有名な言語のような素直な実装だったら、React はクラス型コンポーネントを採用したままだったのではないかと思います。
クラス型から関数型に移るにあたり、props
は関数の引数でいいし、render
メソッドは関数本体そのものでいいととして、その他の state
や内部的な情報(特に ref
)や、render
以外のライフサイクルイベント(有名どころでは mount
や unmount
)などを、一介の関数に過ぎないものが、コンポーネントが生存中に保持し続けるためにはどうすればよいのでしょう?何かよいアイデアは思いつきますか?
前回見たように JS では関数にメンバ変数を仕込むという手段も考えられるところですが、React の中の人はもっと賢そうな手段を採用しました。それが今や普通に市民権を得た Hook です。
そのことを知っているか知っていないかでだいぶ Hook に対する印象も違ってくるはずです。
Svelte5 で useTransition 的なものを実現する
Svelte5 で useTransition 的な UI を 素直に 実現する方法は次のような感じです。
Svelte Play Ground - Live Search with Pending
こんな感じでよいと思う方にはこれで十分でしょう。嫌いではないです(YMMV)。
ちなみにこのサンプルは Gemini 上級 SE が作成したものに私が少し手を加えただけなので、このサンプルに関する質問は Gemini 上級 SE の方にお願い致します。
検索ボタンを付けずに input の変化のみで検索する
useTransition を使用した際の動作とは程遠いものですが、検索ボタンを押さなくても input の変化のみで変化するものは useResource
だけで比較的簡単に実現できます。これをベースに、腕に覚えのある方に、useTransition 的な動作を実現していただけたら嬉しいです。
一応解説します。
let inputValue = '';
= $state('')
としなくてもこれでもリアクティブになるのが面白いです。逆にわかりにくいといえばわかりにくいので Signal を使用した方が統一感が出てよい気もしないでもないです。
const [searchResult] = useResource(
() => inputValue, // source
async (query, { signal }) => { // fetcher
if (query === '') {
// 入力が空の場合は何もしない(必須の処理ではない)
return [];
}
await wait(1000, signal);
const response = await fetch(`https://dummyjson.com/products/search?q=${query}`, { signal });
if (!response.ok) throw new Error(`dummyjson API error: ${response.status} - ${response.statusText}`);
const data = await response.json();
return data.products;
},
{ initialStart: false } // options
);
source を getter 関数にすれば、このようにリアクティブ変数として取り扱われるのが面白いです。前回解説した $effect から refetch が起動しているからこその動作ですが、不思議と言えば不思議ですね。もっとも、何度も言いますが、個人的には Signals で統一した方が分かりしやすい気がします。
refetch()
, abort()
を使用しないため、良くも悪くも中断処理がありえません。すなわち、fetcher 関数に渡される signal も無用の長物ですが、あえてそのまま残してあります。
なんとなくの感想で申し訳ないですが、Svelte は、コンパイルした結果の js が、どのようになるかを想像できる方のほうが使いこなせる気がします。逆にいうと、js でどうなるかよくわからない方は嫌気がさしてしまうかもしれません。
最後に
もしかしたら私が失敗して落ち込んでいると思っている方もいるかも知れませんが、そんな事はありません。むしろ、私の、React と Svelte をその特徴に合わせてうまく使い分けて行く、という目的は十分達成できたと思っております。少しでも皆さんの参考になれば幸せです。
最後に、React や Svelte を学んでいる方々に私への戒めとして贈る言葉です。
"They are not the difficult. But you're making it the difficult to they. They are the simple."
ちなみに the
を付けない方が文法的には正しいそうですが、パロディなのでこれでよいと Gemini 上級 SE のお墨付きです。
実用英語と試験用英語どちらを学ぶべきなのかという気付きを得られたこの頃です。