【学び】:『React HooksとVue Composition APIの比較』
hooksはUIとロジックを疎結合に保つためのもの
v16.8におけるReact Hooksの登場により、コンポーネントの階層構造を変更する事なく、状態を伴うロジックが再利用可能になった事でUIとロジックを疎結合に保つ事ができるようになりました。
様々なUIコンポーネント(主に画面に表示することが責務)に対して、hooksから自由にロジックだけを注入できるようになったのは本当に画期的なことだったと思う。
状態をImmutableに管理するReactと、Mutableに管理するVue
React HooksとComposition APIは根本的に全く異なるAPIであり、筆者もそれに同意します。
その最大の相違点は、状態をMutableに管理するかImmutableに管理するか、という点です。
Reactにおける状態はイミュータブルで、値を直接更新することはできません。setStateによって値が更新されると、コンポーネントは再レンダリングされ、メモ化していない限りは全く新しいオブジェクトに変換されます。
一方でVueではsetCountのようなSetter関数は用意されておらず、直接状態のvalueにアクセスしてミュータブルに値を更新します。
言われてみれば、Reactのステートはセットで渡される更新関数なしには変更できない仕様になっているか。
そして確かに、VueはhogeState.value = 1234
で直接ステートに代入して更新してるね。
ここらへんはReactとVue(Composition API)どっちもある程度触ったことあるからスッと理解できる。
この挙動の原因は、Vueのwatchは遅延実行を採用しているからです。すなわち、第1引数に指定された依存変数(または配列)が変化するまでは副作用は実行されません。この挙動は、watchの第3引数
WatchOption
にimmediate
を指定することでuseEffect
のように即時実行に変更することも可能です。
また、watchの代わりにwatchEffectを使用することで即時実行させることも可能です。watchEffectはwatchと違い依存変数を引数に取らず、関数内で使用されるリアクティブな状態が更新される度にコールバックを発火させます。
これ知らなかった。watch
って即時でステートの値を反映させる方法あったのか。
そしてwatchEffect
という、コールバック内で使われているリアクティブな値に応じて自動発火するのもあったなんて初知りだった。👀
末尾再帰
今回参考にした記事中ではさほど重要ではない「末尾再帰」というワードがなぜか気になってしまって調べたけど、以下の解説記事とても分かりやすくてよかった。
正直、再帰関数を作ることなんて殆どないと思っているけれど、頭の片隅には入れておこう。
Hooksはメモ化された単方向線形リスト
Hooksが単方向線形リストで実装されていることを考えると、ReactにおいてHooksが呼ばれる順番が重要で、条件分岐内でHooksを呼び出せない理由にも納得がいきます。コンポーネント初回マウント以降は再レンダリングの度に過去に作成された線形リストを順番通り走査して差分検出を行うので、もし条件分岐の中でHooksが呼び出された場合はレンダリングの度にNodeの数が変わってしまうという問題を引き起こします。
この部分は、前にuseStateの内部実装を追う記事数本読んでいたときに同じようなことが解説されていたな。毎回同じ順番で呼び出される必要があるってことよね。こういうの「単方向線形リスト」というのね。
Composition APIのインスタンス生成はコンポーネントごとに一度のみ
Hooksと違い、Composition APIではコンポーネントのインスタンス生成時に一度だけsetup()関数を呼び出します。これによってHooksのStale Closureの様な問題は起きません。
Reactを先に学んでいた身からすると、Vueは最初これが厄介だった。
Vueの<script>
内は基本的にコンポーネント生成時の初回だけしか呼ばれないから、console.log
をここに置いても全然ログ出力してくれなくて、「consoleどう使うんや」ってキレてた覚えがある🐟
基本的にはwatchとかで監視するステート指定して使わないといけないのよな。
Composition APIはProxyを用いて状態(ステート)を定義
リアクテビティを実現するためには先ほどtrack関数によって格納されたeffectを呼び出す必要がありますが、この関数を実行する役割はtriggerEffectという関数が担っています。つまり、状態をリアクティブに保つには状態を呼び出すタイミングでtrack関数を、状態が更新されたタイミングでtriggerEffect関数を呼び出せば良いということです。
状態の呼び出し・更新に応じてtrackやtriggerEffectを呼び出すには、ProxyというES6から導入された機能によって実現する事ができます。Proxyとはオブジェクトを別のオブジェクトでラップし、第2引数にハンドラ関数を与える事でget/set操作に独自処理を追加する事ができるオブジェクトです。
なるほど、Vueの内部ではProxy
を用いてget, setを拡張して、getの方には状態(ステート)を呼び出すtrack
を、setの方には状態を更新するtriggerEffect
を内部で実行するようにして状態の取得と更新を管理しているのか。
あ、なるほど。ref()
でステート定義をする時、内部的にはProxyオブジェクトになっているから、たとえプリミティブ値にアクセスする場合でも.value
を付けてステートにアクセスする必要があるのか。はー。
Reactは状態が変わるたびに全て再実行され、Vueは明示的に再実行を指定する
Reactでは状態が変わるたびに以下のInput関数(=Inputコンポーネント)
が上から全て再実行される。
// Inputコンポーネントは状態更新のたびに自動で再実行
function Input() {
const [input, setInput] = useState("");
const inputLength = input.length;
return (
<div className="App">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<div>{inputLength}</div>
</div>
);
}
Vueではコンポーネント生成時に一度だけ<script>
内が実行されるため、状態の更新に応じて値を動的に反映させたい場合はそれを明示的に指定する必要がある。
因みに状態input
はv-model
で状態をバインドしてあるので、変更を検知して画面の表示は切り替わる。
<template>
<div>
<input type="text" v-model="input" />
<div>{{ inputLength }}</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const input = ref("");
const inputLength = input.value.length; // これは初回実行時のみしか動かない(状態inputが更新されても値は更新されない)
// inputの状態に応じて値を動的に算出したい場合はcomputedを用いて状態の変更を検知させる
const inputLength = computed(() => input.value.length);
</script>
これは本当に大きな違いだと思う。
自分はVueと出会ってステートの変更に応じて値を算出する(再計算する)computed
の役割を知ったとき、「Reactではこのcomputed
が何に対応するんだろう?」と思っていた。
いっとき、「useMemo
がこれに対応するのでは?」と思ったこともあったけれど、それは「ステートの値をもとにして行なった計算結果(=値)をキャッシュする」という部分においてuseMemo
と役割が同じと言うだけで、 「ステートの変更をもとにして値を再計算する」 という行為は、Reactにおいては Reactそのものが行なっている(=コンポーネントを全て再実行することで(ステートに依存した値を含めて)値を上から再計算していく) んだと言うことに気付いた。
どこかで言語化したかったことがこの機会で言語化出来たの嬉しい。
素晴らしい記事だった。☺️