コードレベルで徹底比較! 作って比べるReact,Vue.js,Svelte
複数のフロントエンドフレームワークを行ったり来たりしてコーディングしていると、「あれ、こういう処理、このフレームワークだとどう書くんだっけ?」ってなることありますよね。
この記事ではReact, Vue.js, Svelteの3つのフレームワークで全く同じアプリケーションを作り比べてみました。
各フレームワークに入門してみたい人、使ってみたことはあるけどそれぞれ比較しながら見てみたいという人はぜひ見てみてください。
ソースコードへのリンクを張っているので、詳しくはそちらへ。本記事では特に筆者が面白いなぁと思った違いだけピックアップしていきます。
はじめに
- 「Reactはフレームワークじゃない、ライブラリだ!」など、さまざまな意見あるかと思いますが、本記事内ではReact, Vue.js, Svelteいずれもフレームワークとしてとらえます。
- 作成したアプリケーションは、なるべくそれぞれのフレームワークごとでロジックなどに差異がないように作成していますが、実装都合上多少の違いはありますのでご了承ください。
- 本記事では各フレームワークの差異を網羅することは目的としていません。
(とはいえ、アプリケーション開発で必要なケースは結構カバーできているのではないかと思います。)
カバーできている機能は以下のものになります。
機能 | React | Vue.js | Svelte |
---|---|---|---|
リアクティブ値 | useState | ref, reactive | 〇 |
双方向バインディング | 〇 | defineModel, v-slot | bind |
メモ化演算 | useMemo | computed | $ (ReactiveStatement) |
リアクティブ値監視 | useEffect | watch | $ (ReactiveStatement) |
マウント時・アンマウント時処理 | useEffect | onMouted, onUnmounted | onMount, onDestroy |
テンプレート内式埋込 | {} | {{}} | {} |
トランジション | ✕ | transitionタグ | transition属性 |
props | 〇 | defineProps | 〇 |
カスタムイベント(emit) | 〇 | @, defineEmits | createEventDispatcher |
DOM・コンポーネント参照取得 | ref | ref | ref |
子コンポーネントアクセス | ref, forwardRef, useImperativeHandle | ref, defineExpose | ref |
ジェネリクス | 〇 | scriptタグ属性 | scriptタグ属性 |
DOM更新待機 | flushSync | nextTick | tick |
子コンポーネント埋込 | children | slotタグ | slotタグ |
テンプレート内制御構文 | 〇 | if, for | if, for |
※ 表中の「〇」は、他フレームワークと同等の機能を実現しているけれど、フレームワーク固有の記法を持たないことを指します。
実装したアプリケーション
機能概要
よくあるTODOアプリケーションです。
メインとなる3つのフレームワークを使ったアプリケーションと別に、TODOリストを登録するためのAPI用のアプリケーションも一緒に実装しています。
実装したアプリケーションのソースはこちら(@Github )。
機能 | 概要 |
---|---|
新規作成 | TODOの新規作成 |
一覧表示 | 作成したTODOの一覧表示 |
更新 | タイトル、期日、ステータスの更新 |
削除 | TODOの削除(※確認処理はなし) |
検索 | タイトル、期日、ステータスでのTODOリストの絞り込み |
プロジェクト構成
複数のアプリケーションをまとめて実装したかったので、Turborepoを使ってモノレポ構成で組みました。
スタイリングはTailwind.cssを使って、カスタムのクラスを共通に利用できるようにしています。
また、API処理などはどのアプリケーションでも同じなので、共通に参照できるpackagesディレクトリ以下に詰め込んでいます。
/
├───.vscode // vscode settings
├───apps
│ ├───api // TODO登録用APIサーバー
│ ├───app-react // Reactバージョンのアプリケーション
│ ├───app-svelte // Svelteバージョンのアプリケーション
│ └───app-vue // Vue.jsバージョンのアプリケーション
├──packages
│ ├───common // 共通に使用する関数、型、定数など
│ ├───style // 共通のスタイル
│ └───...
└───...configuration-files
主要npmパッケージ
npmパッケージ名 | 使用バージョン |
---|---|
react | 18.2.0 |
vue | 3.4.21 |
svelte | 4.2.7 |
express | 4.19.2 |
tailwindcss | 3.4.3 |
コードのハイライト
ここでは、今回のアプリケーション実装にあたって、個人的に面白いなぁと思った3つのフレームワークごとの記法の違いを紹介していきます。
さすがに全部ピックアップするのは大変なので、主要なところだけですが。
マウント・アンマウント処理
個人的に、useEffectにupdate/mount/unmount時のライフサイクルフックの役割を持たせているのは、Reactのあまり好きじゃないところです。
Vue.js、Svelteはいずれもマウント・アンマウント処理用の専用のフックがあるので、こちらのスタイルの方が明示的で好み。
ちなみに、Vue.jsとSvelteでそれぞれフックの関数名はちょっと違います。
Vue.jsの方は過去形と覚えましょう。
React
マウント時のみ実行したい場合は、useEffectの第2引数を空配列にする。
アンマウント時の処理は、useEffectの返り値としてreturnする。
Vue.js
マウント時のフックはonMounted
アンマウント時のフックはonUnmounted
Svelte
マウント時のフックはonMount
アンマウント時のフックはonDestroy
DOM更新待機
あまり多いユースケースじゃないかもですが。
具体的には、「何らかの処理内でコンポーネントの状態を更新した際に、その更新によるDOMの再描画を待ってからそれ以降の処理を行いたい」というケースで必要になる処理です。
const someFunction = () => {
flushSync(() => {
setIsModalShown(true);
});
// isModalShown=trueとなってモーダルのコンポーネントが表示されてからrefにアクセスしたい。
modalRef.current.focus();
};
Reactの場合は、JSの組み込み関数setTimeoutを使いますが、 Vue.js、Svelteの場合はそれぞれ専用の組み込み関数があります。
(2024-04-14 修正: ReactにもflushSyncという同等の機能の組み込み関数がありました!コメントありがとうございます。)
以下の例は、TODOの編集用モーダルを表示した際に、そのモーダルの表示後に<input>タグにフォーカスを当てたいというケースでこの処理を使っています。
React
Vue.js
Svelte
子コンポーネントアクセス
親のコンポーネントから子コンポーネントの関数を実行したりしたい場合に使う処理です。
ここは、フレームワークごとに結構記法が変わって面白い箇所です。
const ChildComponent = () => {
const doSomething = () => {
// do something...
};
return <div />;
};
const ParentComponent = () => {
const ref = useRef(null);
// 子コンポーネントのdoSomething関数が使いたい!!
useEffect(() => ref.current.doSomething(), []);
return (
<div>
<ChildComponent ref={ref} />
</div>
);
};
React
子コンポーネントはforwardRefでコンポーネント自体をラップしておき、親コンポーネントから渡されるrefをフォワーディングできるようにしておく。
useImperativeHandleに親からアクセスできる関数を定義する。
親コンポーネントはuseRefで該当の子コンポーネントの参照を取得する。(useRefの型引数に渡すのは、子コンポーネントで公開されている、アクセス可能な関数のインターフェース)
Vue.js
子コンポーネントはdefineExposeに親コンポーネントからアクセスできる関数を定義する。
親コンポーネント側ではref<InstanceType<typeof [子コンポーネント]> | null>(null) のようにして子コンポーネントの参照を取得する。
Svelte
子コンポーネントはexport const [親からアクセスできる関数の定義] のようにしてあげると、親コンポーネントからアクセスできる関数を公開できる。
ちなみに、export const ...
という記法自体は、親から読み取り専用の値として定義するための記法のよう[1]。
親コンポーネントからは通常のリアクティブ値と同じようにlet [変数名]で定義しておいた値をbind:thisでコンポーネントにバインドして、定義されている関数を実行できる。
おそらく3つのフレームワークの中で一番この手の処理が楽。
ジェネリクス
ジェネリクスの定義方法は、Vue.js・Svelteに若干クセありです。
Reactの場合は通常のTypescriptと同じ記法でそのまま定義することができます。
利用する側のコンポーネントでは、コンポーネント名+<型>
のように<>
に型引数を渡します。
Vue.js、Svelteはどちらも<script>タグの属性としてジェネリクスを記載するという記法。
React
コンポーネント定義側では通常の関数の型引数と同じ記法でジェネリクスを定義する。
使用する側で型引数を指定するのも、通常の関数を使うのと同じ。
Vue.js
<script generic="...">
という記法で、ジェネリクスを定義する。
Svelte
Vue.jsとほぼ同じで、<script generics="...">
という記法でジェネリクスを定義
(Vue.jsと異なり「generics」なところに注意)
まとめ
React、 Vue.js、Svelteというフロントエンドフレームワークとして近年盛り上がっている(と個人的に感じている)3つのフレームワークについて、記法の比較を交えつつアプリケーションを実装してみました。
3つのフレームワークを網羅的に学習することができ、ソースコードとして形に残すこともできたので、今後はあまり3つのコードの書き方で迷うことはなさそうです。
使えるフレームワークを増やしてみたいなという方は、ぜひ参考にしてみてくださいね!
Discussion
細かいところで失礼します。
についてですが、React にも、Vue, Svelte と同様に flushSync という専用の機能があるので、そちらを使うのが適切だと思います。
コメントありがとうございます!
flushSyncというのがあったんですね、これは初めて知りました。
勉強になります。記事の方も修正いたしました🙇♂️