🥸

コードレベルで徹底比較! 作って比べるReact,Vue.js,Svelte

2024/04/13に公開
2

複数のフロントエンドフレームワークを行ったり来たりしてコーディングしていると、「あれ、こういう処理、このフレームワークだとどう書くんだっけ?」ってなることありますよね。

この記事では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引数を空配列にする。

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-react/src/App.tsx#L130-L132

アンマウント時の処理は、useEffectの返り値としてreturnする。

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-react/src/components/Modal.tsx#L10-L14

Vue.js

マウント時のフックはonMounted

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-vue/src/App.vue#L126-L128

アンマウント時のフックはonUnmounted

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-vue/src/components/Modal.vue#L9-L11

Svelte

マウント時のフックはonMount

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-svelte/src/routes/%2Bpage.svelte#L121-L123

アンマウント時のフックはonDestroy

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-svelte/src/lib/components/Modal.svelte#L7-L9

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

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-react/src/App.tsx#L52-L70

Vue.js

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-vue/src/App.vue#L53-L70

Svelte

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-svelte/src/routes/%2Bpage.svelte#L52-L70

子コンポーネントアクセス

親のコンポーネントから子コンポーネントの関数を実行したりしたい場合に使う処理です。
ここは、フレームワークごとに結構記法が変わって面白い箇所です。

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に親からアクセスできる関数を定義する。

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-react/src/components/FormInput.tsx#L20-L26

親コンポーネントはuseRefで該当の子コンポーネントの参照を取得する。(useRefの型引数に渡すのは、子コンポーネントで公開されている、アクセス可能な関数のインターフェース)

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-react/src/App.tsx#L21

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-react/src/App.tsx#L187-L193

Vue.js

子コンポーネントはdefineExposeに親コンポーネントからアクセスできる関数を定義する。

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-vue/src/components/FormInput.vue#L18-L20

親コンポーネント側ではref<InstanceType<typeof [子コンポーネント]> | null>(null) のようにして子コンポーネントの参照を取得する。

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-vue/src/App.vue#L20

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-vue/src/App.vue#L185-L190

Svelte

子コンポーネントはexport const [親からアクセスできる関数の定義] のようにしてあげると、親コンポーネントからアクセスできる関数を公開できる。
ちなみに、export const ...という記法自体は、親から読み取り専用の値として定義するための記法のよう[1]

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-svelte/src/lib/components/FormInput.svelte#L6-L8

親コンポーネントからは通常のリアクティブ値と同じようにlet [変数名]で定義しておいた値をbind:thisでコンポーネントにバインドして、定義されている関数を実行できる。
おそらく3つのフレームワークの中で一番この手の処理が楽。

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-svelte/src/routes/%2Bpage.svelte#L19

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-svelte/src/routes/%2Bpage.svelte#L170-L175

ジェネリクス

ジェネリクスの定義方法は、Vue.js・Svelteに若干クセありです。
Reactの場合は通常のTypescriptと同じ記法でそのまま定義することができます。
利用する側のコンポーネントでは、コンポーネント名+<型>のように<>に型引数を渡します。

Vue.js、Svelteはどちらも<script>タグの属性としてジェネリクスを記載するという記法。

React

コンポーネント定義側では通常の関数の型引数と同じ記法でジェネリクスを定義する。

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-react/src/components/FormSelect.tsx#L8-L22

使用する側で型引数を指定するのも、通常の関数を使うのと同じ。

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-react/src/App.tsx#L202-L208

Vue.js

<script generic="...">という記法で、ジェネリクスを定義する。

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-vue/src/components/FormSelect.vue#L1-L6

Svelte

Vue.jsとほぼ同じで、<script generics="...">という記法でジェネリクスを定義
(Vue.jsと異なり「generics」なところに注意)

https://github.com/y-h-haru04/react-vue-svelte/blob/main/apps/app-svelte/src/lib/components/FormSelect.svelte#L1C1-L6C10

まとめ

React、 Vue.js、Svelteというフロントエンドフレームワークとして近年盛り上がっている(と個人的に感じている)3つのフレームワークについて、記法の比較を交えつつアプリケーションを実装してみました。
3つのフレームワークを網羅的に学習することができ、ソースコードとして形に残すこともできたので、今後はあまり3つのコードの書き方で迷うことはなさそうです。
使えるフレームワークを増やしてみたいなという方は、ぜひ参考にしてみてくださいね!

脚注
  1. 参考: Svelte公式 ↩︎

Discussion

Honey32Honey32

細かいところで失礼します。

DOM更新待機

についてですが、React にも、Vue, Svelte と同様に flushSync という専用の機能があるので、そちらを使うのが適切だと思います。

const someFunction = () => {
    flushSync(() => {
        setIsModalShown(true);
    });
    // ここから下のコードは、setIsModalShown の変更が dom に反映されていることが保証される
    modalRef.current.focus();
};

https://ja.react.dev/reference/react-dom/flushSync

EngineerCookEngineerCook

コメントありがとうございます!
flushSyncというのがあったんですね、これは初めて知りました。
勉強になります。記事の方も修正いたしました🙇‍♂️