このコードはいつ実行されるんだっけ?
JavaScriptはサーバでもクライアントでも同じ言語を使って開発できるというのが売りのひとつである一方で,「このコードはいつどのタイミングで走るんだっけ?」という疑問・意識を持っておかないと,意図したとおりにコードが動かなくなって辛いことになる.特に React, Vue.js を使ったWebフロントエンド開発,さらに Server-side Rendering などが絡むとやばい. Server vs Client
最も根源的な部分.抽象度高めに表現しているが有り体にいえばNode.jsかWebブラウザかという区別.DenoやWorkerが入るとさらに複雑になるがいまあまり詳しくないので省略.そのうち書く. グローバルオブジェクトはNode.jsでは global ,ブラウザではwindow.typeof window === "undefined" のような式で区別する手もあるが,新しめのECMAScriptが使える環境なら globalThis という標準化されたグローバルオブジェクトが利用できるのでそれを使えれば良い.
ブラウザ,Node.jsともにECMAScriptに含まれていないオブジェクトがグローバルに追加されていることが多い.ブラウザではDOM などのWeb標準にあるAPIが生えている (documentとか) し,Node.jsはNode.jsでシステムにアクセスするようなオブジェクトがある (processとか). いわゆるモダンフロントエンド開発では実運用でサーバでのコード実行が必要になるか否かにかかわらず,開発ツールを動かすための環境としてNode.jsが利用される.そこでツールの設定を行う際に,たとえば webpack なら webpack.config.js のようなスクリプトを書く必要が生じる.当然のことかもしれないが,webpackはNode.jsで実行されるため,webpack.config.js もNode.js で実行される.なので window や fetch といったAPIにはアクセスすることができない. コンポーネントのライフサイクル
ReactやVueのコンポーネントにはライフサイクル上で多くのイベントが存在し,どこに書いたコードがどのタイミングで走るのかをちゃんと理解しないと終わる.特にReact Hooks,Vue Composition APIは一見してイベントっぽくないのでちゃんと理解すべき.これは単純なイベント駆動のアプローチが将来的にすっげえやりづらい設計になることの示唆である(?).
Reactの昔懐かしいClassコンポーネントでは,コンポーネントを描画するときにはrenderメソッドが呼ばれる.render でReact要素 (多くはJSXで表現される) を返す.あとはコンポーネントがマウントされるとかアンマウントされるといったイベントで発火するメソッドを書いていく.状態 (state) はクラスのメンバとして持つ. code:Foo.tsx
class Foo extends React.Component {
count = 3;
increment() {
this.count++;
}
render() {
return (
<div>
<button onClick={this.increment()}>Add</button>
<span>{this.count}</span>
</div>
);
}
}
Vue Options APIはなんかうろ覚えだけどこんなだっけ.
code:Foo.tsx
defineComponent({
data() {
return { count: 0 };
}
increment() {
this.count++;
}
render() {
return (
<div>
<button onClick={this.increment()}>Add</button>
<span>{this.count}</span>
</div>
);
}
});
これらのAPIではコンポーネントはオブジェクトであり,ライフサイクルフックは特定の名前のメソッドとして登録されている.しかし,ある名前のメソッドは1つのオブジェクトに1つしか宣言できないのに対して,コンポーネントがもつ機能は複数のイベントにまたがって実装する必要がある.
React Hooks では変わり,レンダーのたびにコンポーネントの実体である関数のbody部分が呼ばれる.その中で const let を使ってstateや関数を宣言する.こうすることでstateや関数の宣言部分を関数 (カスタムフック) として分割できるようになる.ちなみにReactの useState で宣言されるstateは実際にはイミュータブルで,レンダーが発生しstateを含む関数コンポーネントが呼ばれるたびに新しくstateの宣言・束縛が行われることで,あたかも状態が変化しているようにはたらいている.
code:FooHooks.tsx
const Foo: React.FC = () => {
// ここはレンダーのたびに呼ばれる
// この関数はbuttonのクリックイベントのたびに呼ばれる
// count がイミュータブルに変更されていることに注目
const increment = () => setCount(count => count + 1);
return (
<div>
<button onClick={increment}>Add</button>
<span>{count}</span>
</div>
);
};
Vue Composition API も React Hooks と同じようにステートや関数を変数として扱うことで,それらの宣言を関数として切り出せるようにしている.React Hooks との差異点は,宣言を行うライフサイクルが異なるということ.Vue Composition API でステートなどの宣言を行う setup は beforeCreate の前に実行される.ここから関数を返すと,それがレンダーのたびに実行される.Composition APIにおけるstate (リアクティブな値) は一回だけ宣言され,プロパティ value への代入などの操作によってリアクティブにレンダーが引き起こされる. code:FooCompo.tsx
defineComponent({
setup() {
// ここはbeforeCreateのさらに前に1回だけ実行される
const count = ref(0);
// この関数はbuttonのクリックイベントのたびに呼ばれる
// count.value にミュータブルな変更を加えていることに注目
const increment = () => count.value++;
// この関数はレンダーのたびに実行される
return () => (
<div>
<button onClick={increment}>Add</button>
<span>{count.value}</span>
</div>
);
}
});
ところで,ReactやVueにはコンポーネントの内容をNode.js環境上でHTMLとして吐き出すことで表示の速さを実現する Server-side Rendering という技術がある.Static Generation あるいは SSG とか呼ばれる技術もやっていることは同じ.これらの技術が絡むと厄介なのが,コンポーネントがブラウザのAPIを使用しているとSSRで死ぬということ.特に localStorage への読み書きとか fetch とか window.addEventListener とかが該当するかな.Node.jsで死なないようにするためには,これらのコードをブラウザ側でのみ実行させるようにしなければならない.Reactでは useEffect. code:fetch.tsx
export const Foo: React.FC = () => {
useEffect(() => {
setName(localStorage.get("name") ?? "");
});
return <div>Hi, {name}</div>
};
Vue はまだ調べてない.