ReactやVueとかの記事を見ていると、useMemoしろとuseMemoするなとか、イベントハンドラにuseEffectを使えとイベントハンドラにuseEffectを使うなとか、どうすりゃええねんという話をよく見かけます。
またなんか無限レンダリングされるとかなんか2回計算されるとか、そういう困った記事も良く見かけます。
なんでこんな問題をわざわざこっちで面倒みないといけないの?
そもそもリアクティブってお前らが勝手に持ち込んだ概念なんだから、解決もライブラリ側で勝手にやってくれよ。
どうしてユーザがいちいち対応しないといけないんだ?
そこで登場するのが、再レンダリング問題を独自のアプローチで完全解決したJavaScriptフレームワークCrank.jsです。
import {renderer} from "@b9g/crank/dom";
function Greeting({name = "World"}) {
return <p>Hello {name}.</p>;
}
function RandomName() {
const names = ["Alice", "Bob", "Carol", "Dave"];
const randomName = names[Math.floor(Math.random() * names.length)];
return (
<div>
<Greeting name={randomName} />
{
<button onclick={() => this.refresh()}>Random name</button>
}
</div>
);
}
renderer.render(<RandomName />, document.body);
よくあるソースコードですが、たったひとつ他のフレームワークと異なる部分として、refresh()がCrank.jsをCrank.jsたらしめている最大の特徴です。
ということで以下はCrank.jsの設計思想を語った公式ブログ、Why Be Reactive?の紹介です。
Why Be Reactive?
リアクティブフレームワークは、UIの自動更新を約束するかわりにバグやパフォーマンス問題の温床になります。
Crankの明示的なrefresh()は制限ではなく、強力なツールです。
この記事では、リアクティブのよくある落とし穴を検証し、Crankがリアクティブを採用しなかった哲学的根拠を解説します。
Crank.jsを始めてリリースしたとき、暖かい反響をいただきました。
Reactコアチームからの応援ツイート、GitHubのスター、RedditやHackerNewsで多くの話題が繰り広げられました。
残念ながら私はその後時間を無駄にしてしまい、Crankは今ではあまり使われていません。
それでも、長年Crankに携わってきたことに誇りを感じています。
厄介なバグへの対処、API設計、パフォーマンスの向上など、基本的なものから高度なメンテナンスまであらゆる作業をこなしてきました。
しかし最も困難だったことは、技術的な面ではなく、社会的な面でした。
開発者に、新しいフレームワークを採用するという大きな一歩を踏み出してもらうためにはどう説得すればいいか、ということです。
最初の売り文句のひとつが、CrankはJavaScriptそのものなフレームワークであるということでした。
コンポーネントは関数であり、非同期関数やジェネレータも関数であり、すなわちコンポーネント内で直接Promiseをawaitしたり、状態をローカル変数で保持したりできます。
直感的に、これは実にJavaScriptです。
JSXはJavaScriptではないと言いたがる人々を丸め込むため、わざわざテンプレートタグも用意しました。
はて、これだけで売り文句は十分でしょうか?
長年Crankを使っていて、もっとよい売り文句があることに気が付きました。
Crankは、一般的なリアクティブの定義から見るとリアクティブではなく、むしろ非リアクティブとさえいえるのではないでしょうか。
ほぼ全てのフレームワークが自らをリアクティブであると主張しており、フレームワーク同士の比較もリアクティブ抽象化に基いて行われるほどの現在、Crankは実に型破りです。
こんにちのフレームワークはSignals・Stores・Observablesといったリアクティブプリミティブを中心に構成されています。
コンポーネントはstateを作成し、フレームワークはそれを自動的に再レンダリングします。
そのため、リアクティブ抽象化を提供しないということは、それが不完全なフレームワークであると言っているのと同義のように見られるかもしれません。
では、何故わざわざCrankは非リアクティブであると主張し、あまつさえそれをセールスポイントにしようとしているのでしょうか?
抽象的な話ばかりだと困るので、具体的な定義とコードの例を挙げましょう。
Webフレームワークの文脈におけるリアクティブ抽象化とは、「stateが変化した際にフレームワークがviewを更新する」機能であると定義できます。
viewとstateの定義は任意ですが、両者が同期した状態を維持するのがリアクティブ抽象化です。
この最も一般的であろう定義に照らし合わせても、初期のCrankはリアクティブではありませんでした。
たとえば、初期のCrankではタイマーを以下のように定義していました。
function *Timer() {
let seconds = 0;
const interval = setInterval(() => {
seconds++;
this.refresh();
}, 1000);
try {
for ({} of this) {
yield <div>{seconds}</div>;
}
} finally {
clearInterval(interval);
}
}
Timerコンポーネントでは、唯一のstateはsecondsです。
この値を変更したあと、viewを更新するためには別途メソッドrefresh()を呼び出す必要があります。
初めてCrankを見た際、このコードに反発するリアクティブ派も多数いました。
「stateの更新後にrefreshの呼び出しを忘れる欠陥がある、これはfootgunだろう?」
確かにそうです。
Crankの初めてのユーザだった私も、stateを変更したあとにrefreshの呼び出しを忘れるバグに悩まされました。
しかし我々は、リアクティブ抽象化を導入するのではなく、refreshを忘れないようにすることにしました。
幸い、refreshにコールバックを渡すことで、この問題を解決できることについ最近気が付きました。
import {renderer} from "@b9g/crank/dom";
function *Timer() {
let seconds = 0;
const interval = setInterval(() => this.refresh(() => {
seconds++;
}), 1000);
for ({} of this) {
yield <div>{seconds}</div>;
}
clearInterval(interval);
}
renderer.render(<Timer />, document.body);
このAPIの実装は簡単だったため、Crank0.7に土壇場で追加されました。
またforループをtryで囲む必要もなくなりなっていることにも注目してください。
これはCrank0.5で導入された、ちょっと使い勝手が上がる機能です。
stateの変更をコールバック内に置くことでrefreshの呼び忘れがなくなり、コールバック内のコードは再レンダリングされることが明示的に識別されることになります。
これは、もっと早く思いついていてもよかったアイデアです。
実のところ、このアイデアを思い付いたのはClaude Codeでした。
Crankコンポーネントの開発をしているのにReactのハルシネーションを起こしてきて厄介だったところに、彼はAPIにもハルシネーションを起こしたのです。
Claudeがrefreshのコールバックを思いついたことに感謝すると同時に、自力でもっと早く思いつけなかったことを無念に思います。
Bug severity analysis
しかし、なぜrefreshを忘れることを許容するのでしょうか。
リアクティブ抽象化は、stateとviewを自動的に同期させることで、同期忘れのバグを排除することを約束します。
しかし後述するように、この解決策は、別の問題を引き起こします。
私が非リアクティブは構造の欠陥ではないと考える理由を理解するために、まずはバグの深刻度について考えてみましょう。
バグの深刻度は、次の2点で評価できるでしょう。
・そのバグは簡単に発見できるか。
・そのバグは簡単に修正できるか。
この2つの質問は、そのバグがいつエンドユーザに見つけられるか、そしてアプリケーションがいつまで動かないままなのか、を決定します。
Crankの場合、この二つの質問の答えはどちらも"Yes"です。
refresh()を忘れることによるバグは、画面が更新されないことからすぐに発見できます。
そして修正もrefresh()を追加するだけです。
興味深いのはここからです。
リアクティブ抽象化はいずれも古式なレンダリング問題を排除できると主張していますが、しかしリアクティブ抽象化はいずれも落とし穴を抱えています。
次のセクションでは、Solid・Vue・Svelteなどのフレームワークを用いて具体例を説明します。
Losing Reactivity
Solid.jsの悪名高い例を見てみましょう。
import {render} from "solid-js/web";
import {createSignal} from "solid-js";
// ❌ 反応しない
function BrokenDisplay1({seconds}: {seconds: number}) {
return <div>{seconds} second{seconds === 1 ? "" : "s"} elapsed</div>;
}
// ❌ 反応しない
function BrokenDisplay2(props: {seconds: number}) {
const minutes = props.seconds / 60;
return (
<div>
<span>{props.seconds} second{props.seconds === 1 ? "" : "s"} elapsed</span>
{" "}
<span>({minutes.toFixed(2)} minutes)</span>
</div>
);
}
// ✅ 反応する
function WorkingDisplay1(props: {seconds: number}) {
return <div>{props.seconds} second{props.seconds === 1 ? "" : "s"} elapsed</div>;
}
// ✅ 反応する
function WorkingDisplay2(props: {seconds: number}) {
const minutes = () => props.seconds / 60;
return (
<div>
<span>{props.seconds} second{props.seconds === 1 ? "" : "s"} elapsed</span>
{" "}
<span>({minutes().toFixed(2)} minutes)</span>
</div>
);
}
function Timer() {
const [seconds, setSeconds] = createSignal(0);
setInterval(() => setSeconds(seconds() + 1), 1000);
return <>
<BrokenDisplay1 seconds={seconds()} />
<BrokenDisplay2 seconds={seconds()} />
<WorkingDisplay1 seconds={seconds()} />
<WorkingDisplay2 seconds={seconds()} />
</>;
}
render(() => <Timer />, document.getElementById("app")!);
Solidはシグナルとストアという2種類のリアクティブ抽象化を用いており、DOMの更新を行います。
Solidではコンポーネントは関数ですが、コンポーネントに渡すpropsはリアクティブなストアです。
DOMを最新の状態に維持するため、Solidは特殊なbabelレンダラーを必要とします。
このレンダラーは、JSXのステート読み取り時に、再計算ロジックをトリガーします。
この手法は、propsストアから値を取り出したり、JSXの外で値を変更したりすると即座におかしくなります。
さきほどの深刻度判定を使ってこのバグを考えてみましょう。
単純なケースであれば、propsから値を取り出さない、計算するにはコールバックを使用する、というLinterルールを使用すれば簡単に発見できます。
しかし、複雑なアプリケーションの中にはこのLinterルールの網をすり抜けてしまうエッジケースも存在します。
このリアクティブフレームワークのエッジケースについては、losing reactivityで調べると山ほど出てきます。
では、このバグは簡単に修正できるでしょうか。
答えはNo。
フレームワークごとに、どのコンテキストがリアクティブであるか全てのルールを把握しなければならないからです。
propsの操作は非常に複雑であるため、分割や結合といった基本的なタスクを実行するだけでも専用のユーティリティを使用する必要があります。
propsは、Crankでは単なる普通のオブジェクトです。
Crankの明示的refreshモデルでは、この種類のバグは存在しません。
propsは単なる値です。
propsから値を取り出したり、計算したり、他の関数に渡したりできますし、全て普通のJavaScriptで記載します。
コンポーネントを更新したければ、refresh()を呼びます。
すぐ壊れる目に見えないリアクティブコンテキストは、存在しません。
The Deep Reactivity Performance Trap
次はVue.jsを考えてみましょう。
Vueも、Solid同様に読み取りを制御するリアクティブ抽象化を導入しています。
リアクティブオブジェクトの子要素やプロパティも再帰的にリアクティブするプロキシを提供しています。
これにより、深いネストの先の状態が変更されてもDOMを更新することができます。
import {ref} from "vue";
const state = ref({
todos: [
{id: 1, text: "Learn Vue", completed: false, metadata: {priority: "high", tags: ["learning"]}},
// ... imagine 1000 more todos with nested objects
],
filters: {status: "all", search: ""},
ui: {selectedTodo: null, theme: "light"}
});
state.value.todos[0].completed = true; // ✅ UI updates
state.value.todos[0].metadata.priority = 'low'; // ✅ UI updates
const {ui} = state.value;
// ✅ UI updates Solidと異なり、これも更新される
ui.selectedTodo = state.value.todos[0];
privateメンバーでは動作しないという事実は無視しましょう。
これは言語仕様の問題です。
また、プリミティブ型に使用できないという事実も無視しましょう。
これこそがVueのreactive()とref()をややこしくしている原因です。
また、大きなオブジェクトや配列に深くまでプロキシすることは、パフォーマンスのボトルネックになります。
そのため、Vueでは大きなオブジェクトに対しては、トップレベルだけリアクティブする浅いプロキシにすることを推奨しています。
Vue公式フレームワークのベンチマークコードもそのように作られています。
深いネストの奥までプロキシできる全てのフレームワークは、ベンチマークではその機能を使わないように避けています。
import {ref, shallowRef} from "vue";
// ❌ ref()すると全てがリアクティブになる
// const rows = ref([])
// ✅ パフォーマンスのためにこうすべき
const rows = shallowRef([])
function setRows(update = rows.value.slice()) {
// 手動でトリガー
rows.value = update
}
function update() {
const _rows = rows.value
for (let i = 0; i < _rows.length; i += 10) {
_rows[i].label += ' !!!'
}
// 手動でトリガー
setRows()
}
Vueでは、このようなパフォーマンス問題を回避するためにshallowRef()やmarkRaw()といった抜け道を提供しています。
しかしそのかわり、どんなネストの奥でも自動で再レンダリングしてくれるという便利さを切り捨て、どこが再レンダリングされ、どこが再レンダリングされないかを開発者自身が把握しなければならなくなりました。
そのため、リアクティブかどうかを判断するためにisReactive()のようなユーティリティが必要となってしまいました。
それでは深刻度判定してみましょう。
リアクティブ状態はデータ構造に乗っておらず、パフォーマンス向上のために削除される可能性があるため、このバグは発見が困難です。
さらに、どうしてそのデータはリアクティブなのか、あるいはリアクティブでないのかを突き止めなければならないため、修正も困難です。
そして実装者は、そのデータがリアクティブかそうかを自身で調べる必要があります。
それではCrankを考えてみましょう。
Crankはネストの深さを気にせず、単にrefresh()が呼ばれたら更新します。
Effects and Infinite Loops
リアクティブ脳に陥ると、関数型プログラマがあらゆるものをモナドにするのと同じように、プログラミングをリアクティブに捉え始めます。
プログラムの全ての状態はリアクティブであり、ステートもリアクティブです。
そしてリアクティブ状態は、何かを変更するたびに自動実行されるeffectを使って読み取ります。
フレームワークによるDOMの更新は、多くのeffectのひとつにすぎません。
サードパーティライブラリの呼び出し、命令型キャンバス描画など、他の処理を行うeffectも任意に記述できます。
Svelteの初期バージョンには、実にCrank的だと考えていたリアクティブAPIがありました。
Svelteコンパイラは、コンポーネント内のステートへの代入を監視し、代入によって再レンダリングをトリガーするようにしました。
ネストされた状態の更新や、実行時のリアクティブなどは存在せず、代入イコール更新です。
<script>
let todos = [
{id: 1, text: "Learn Svelte", completed: false, metadata: {priority: "high"}}
// more todos...
];
function toggleTodo(id) {
// ❌ 反応しない
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed;
// ✅ 反応する
todos = todos; // or todos = [...todos]
}
function addTodo() {
// ❌ 反応しない
todos.push({id: Date.now(), text: 'New todo', completed: false});
// ✅ 反応する
todos = todos;
}
</script>
{#each todos as todo}
<div>
<input type="checkbox" checked={todo.completed} on:change={() => toggleTodo(todo.id)} />
{todo.text}
</div>
{/each}
残念ながらSvelteのメンテナーはこのリアクティブの欠如が問題だと考えたようで、runesと呼ばれる特別な構文を開発しました。
$state()や$derived()のように$で始まる関数を呼ぶことで、リアクティブを持つ変数を作成できます。
また$effect()などの関数で更新をリッスンできます。
<script>
let todos = $state([
{ id: 1, text: 'Learn Svelte', completed: false, metadata: { priority: 'high' } }
]);
function toggleTodo(id) {
// ✅ 反応する
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed;
}
function addTodo() {
// ✅ 反応する
todos.push({ id: Date.now(), text: 'New todo', completed: false });
}
</script>
{#each todos as todo}
<div>
<input type="checkbox" checked={todo.completed} onchange={() => toggleTodo(todo.id)} />
{todo.text}
</div>
{/each}
これらのrunesはコンパイラ固有の機能ですが、メモリやアセンブリへの低レベルアクセスではなく、高レベルのリアクティブ抽象化を提供します。
問題は、effectを使用すると極めて容易に無限ループになることです。
<script>
let password = $state('');
let attempts = $state(0);
let isSubmitting = $state(false);
// ❌ 無限ループ
$effect(() => {
if (isSubmitting && password !== 'hunter2') {
attempts++; // effectが再反応する
setTimeout(() => {
isSubmitting = false;
password = '';
}, 2000);
}
});
function handleSubmit(e) {
e.preventDefault();
if (password) {
isSubmitting = true;
}
}
</script>
<form onsubmit={handleSubmit}>
<input
type="password"
bind:value={password}
disabled={isSubmitting}
/>
<button type="submit" disabled={isSubmitting || !password}>
{isSubmitting ? 'Checking...' : 'Login'}
</button>
<p>Failed attempts: {attempts}</p>
</form>
このコンポーネントは、$effect()ルーン内で$state()の読み込みと書き込みを両方行っているため、コールバックが再発動してスタックが爆発します。
リアクティブ信者はしばしば、リアクティブによってプログラミングはスプレッドシートになる、すなわち各セルが更新されると他のセルも連動して更新される、という主張をします。
これは、彼らにExcelファイルを窓から投げ捨てる経験がなかったことを意味します。
大量の計算セルを含むスプレッドシートは非常に重くなり、ときには全く開くことができないこともあります。
全てのeffectコールバックは、書き込みによって別の読み込みが反応し、結果として無限ループに陥る可能性があります。
Excel同様、Svelteもヒューリスティックや抜け道を提供しており、ほとんどの場合は無限ループを回避しようとしますが、それでもクラッシュすることがあります。
Svelteでの解決策は、$effect()ルーンを使用しない、$effect()ルーン内での更新には気を付ける、untrack()関数を使って反応しないようにすることなどです。
// ✅ untrack()すると反応しない
$effect(() => {
if (isSubmitting && password !== 'hunter2') {
untrack(() => {
attempts++;
});
// 以下略
}
});
それでは深刻度判定しましょう。
このバグは簡単に発見できるでしょうか?
通常は一瞬でスタックが吹き飛ぶのですぐにわかりますが、複雑なコンポーネントでは滅多に発火しないエッジケースが存在するかもしれません。
また$effect()は中に入ってきた全てのコードを汚染するため、コールバック自体がルーンに書き込んではいけないのはもちろん、ネストされた全ての呼び出しもルーンに書き込みを行わないようにしなければなりません。
このeffect汚染は目に見えないため、ユーザが気を付けてコードを書くか、最初からuntrack()で防衛するかしなければなりません。
そしてその防衛によって、今度は必要な時にeffectが発生しなくなることもあります。
これらの無限ループバグは、effect内でデバッグを行うとリアクティブの挙動が変わる場合があるので、修正が困難です。
単にログを出力するだけという一見無害な動作が、無限ループを引き起こすことがあるのです。
その無限ループは、ログがコメントアウトされている場合は発生しません。
そのため、関数の動作を変更せずにデバッグを行うことが困難です。
Crankには無限ループを引き起こすようなeffectは存在しません。
むしろeffectが存在しません。
それでも無限ループが発生する可能性はありますが、それはおそらく開発者がそう書いたからであり、エラーはスタックトレースにはっきり残ります。
皮肉なことに、リアクティブ抽象化は手動による更新管理を不要にするという謳い文句だったにもかかわらず、あらゆるフレームワークはそれぞれ独自の更新管理が必要となります。
Solidはpropsを安全に処理するためにsplitPropsやmergePropsが必要となり、Vueはパフォーマンス問題を避けるためにshallowRefやmarkRawを使わざるを得ず、Svelteは無限ループを避けるためにuntrackが不可欠です。
このようなAPIが存在する理由は、リアクティブが更新の問題をいまだ根絶できていないからです。
Executional Transparency
なぜCrankは明示的なrefreshをさせるのか、そして最近refreshコールバックを思いつくまで『refresh呼び忘れた問題』を解決できなかったのか。
これについて考えると、あまり言及されることのない『実行の透過性』というコンピューティング哲学に触れなければなりません。
実行の透過性は、参照の透過性と対になる概念と言えます。
参照透過性は、stateに副作用がなく、不変の変数とデータ構造を使用するというコード形式です。
この制約の結果、コード内でデータがどのように流れるかが見えやすくなります。
なぜなら、データを秘かに変換する方法がないからです。
// 参照透過。同じ入力には同じ出力が返る
const add = (a, b) => a + b;
// 参照透過ではない。値によって出力が変わる
let counter = 0;
const increment = () => ++counter;
参照透過性がデータの変化を把握することだとすれば、実行透過性はコードがいつ実行されるかを把握することです。
フレームワークは、コードがAPIを呼び出すのではなく、APIがコードを呼び出す「制御の反転」を行うための概念として定義されてきました。
これはコードの実行透過性を高めるために重要な役割を果たしています。
Crankのコードは、制御を明示するがゆえに実行透過的になります。
すなわち、コンポーネントは、親コンポーネントがrefreshされるか、自身がrefreshされた場合にのみ実行されます。
実行透過性を優先するというCrank哲学の根底にあるのは、Reactに対するものです。
Reactはこれまで挙げてきたいずれのフレームワークより最もリアクティブでないにも関わらず、どういうわけか実行透過性も最も低くなっています。
長年にわたり、Reactは実行透過性を軽視してきました。
たとえばレンダリングに副作用が含まれないことを確認するためにコンポーネントを二重にレンダリングし、コールバックがコールバックを返し、スケジュールアルゴリズムは気まぐれでレンダリングを実行し、useEffect()・useSyncExternalStore()・useTransition()といった紛らわしいAPIを実装しました。
Reactは開発が進めば進むほど、コンポーネントをより多数のコールバックに分解し続けることで、コンポーネントがいつ実行されるのかの不透明度を増し続けています。
これはつまり、Reactの開発者が、意図していたかそうでないかはともかく、実行透過性よりも参照透過性のほうが大事だと認識していたからでしょう。
しかし実際は、参照透過性と実行透過性の両方を兼ね添えたコードも実現できるはずです。
これらは相反する概念かもしれませんが、決して逆相関しているわけではありません。
Reactエコシステムにおいては、コードの実行タイミングに関するブログ記事、間違い、過剰なレンダリングを抑えるためのWhy Did You Renderのようなツールが無数に存在します。
そしてコンポーネント内に定数値を保存するだけという単純極まりないタスクですら、ベストプラクティスを巡る論争が絶えません。
useCallbackをいつ使うべきかなどという記事が未だに書かれ続けているのです。
Non-reactivity Is a Superpower
正直なところ、リアクティブ抽象化の欠点と、Webをリアクティブにするために費やされた無駄な時間を考えると、リアクティブしない選択をしたフレームワークがほとんど存在しないことに驚きを禁じえません。
そして夜中にふと思うのです。
ほとんどが男性で構成されているフレームワークのメンテナであれば、いつ更新するかを開発者に委ねるのが最も簡単な解決策だと考えないことも不思議ではないでしょう。
FacebookやGoogleのような広告会社に雇われているフレームワークのメンテナであれば、更新タイミングを開発者に明示的に出させるのではなく、いつ更新するかを推測することが必要だと考えるのも無理はありません。
おそらくWebの問題に対する視点が異なるからでしょう。
あらゆるフレームワークはますます完璧なリアクティブソリューションを追い求めており、Reactのコンパイラは文字通りすべての変数をキャッシュに格納し、ステップ実行によるデバッグを不可能にし、そしてTodoMVCの完成度はより美しくなり続けています。
しかし、Webの最先端はTodoMVCではありません。
アニメーション、仮想リスト、スクローリーテリング、編集可能なコードエディタ、WebGLレンダラー、ゲーム、WebSocketを使ったリアルタイムアプリ、大規模データビジュアライゼーション、動画エディタ、地図などなど、Webプラットフォームで実現されるクールで高難易度のものはいくらでもあります。
リアクティブ抽象化は、これらのいずれにも全く役立ちません。
Crankを使えば使うほど、レンダリングタイミングを明示的に制御できることの便利さを実感します。
コードが実行される理由を把握できます。
必要な時に必要なだけレンダリングできます。
これらの意思決定を中継するけど間違いやすい、リアクティブ抽象化はありません。
Crankに5年間1875回のcommitを繰り返す間、「もしコンポーネントが単なる関数だったらどうだろう」と考え続けてきました。
その結果、Crankは今やかなり優れたユーザエクスペリエンスを提供することができていると思います。
ここまで目を通したあなた、ぜひプレイグランドにアクセスして、クールなサンプルを試してみてください。
我々には、Webをよりインタラクティブに豊かに表現する力があります。
そして、それは「どうしてリアクティブなのか?」の問いから始まるのです。
感想
現在主流となっているあらゆるリアクティブフレームワークは致命的な欠陥を孕んでいる。
Crankは独自のアプローチによって、このリアクティブ問題を解決した。
という主張です。
すなわち、リアクティブの欠陥が解決すればCrankの存在意義もなくなるわけですが、Reactを筆頭としてあらゆるリアクティブフレームワークが未だ欠陥の根絶に成功していないので、そのときが来るのはまだまだ当分先のことでしょう。
もちろん『ちゃんと書けば』リアクティブフレームワークでも問題なく動作するわけですが、その『ちゃんと書く』ができないから問題なんだっての。
ほんの少しでも間違った書き方をしようものならすぐにバグって警察が出張ってきてギャワギャワ叫び始めますからね。
最初から間違った書き方ができないようにしろよって話ですわ。
そもそもリアクティブって、変数値を書き換えると画面も更新される、というだけの話だったはずなのに、以来ありとあらゆる枝葉末節が付け足され続けた結果、今や誰も彼もが枝葉末節の話しかしていません。
ということで何故かレンダされないとか何故か2回レンダされるとか何故か無限にレンダされるとかの対策に明け暮れたなど、枝葉末節に疲れ果てたという人はCrankを試してみてはいかがでしょうか。
なお最終章の広告会社がどうこうとか最先端がどうこうってあたりはいまいちよくわかりませんでした。
ChatGPTやニコニコ動画はReactですし、GitLabやGoogleドキュメントはVueです。
たしかに何の価値もないTODOアプリがそこらじゅうに溢れていることは事実ですが、それ以上にTODOよりずっと有用で高度な用途や技術も溢れています。
この章をどうして執筆してしまったのか。