owned mediaウェブ制作に役立つコンテンツを発信中!

Vue.js(Options API)からReactへの移行でみる違いと比較 #1:コンポーネント

最終更新日: Update!!
Vue.jsもReactもJavaScriptのフレームワークとしてよく使われています、両方とも別物になりますが要件実装するために同じような機能が用意されています。ただ書き方やルールなど扱いが異なる部分はもちろんあるので、移行する際にはそれぞれどのように対応するかなどを覚えておく必要がありますね。今回はVue.jsで作成したアプリをReactに置き換えてみる中で両者の違いや比較をしてみたいと思います。   検証にあたってVue.jsは3系でOptions APIでの実装を、Reactは17系で関数コンポーネントでの実装を前提に、機能も見た目も同じの簡単なTODOリストのアプリを作成しています。本記事ではVue.jsやReactの開発環境については割愛させていただきますので過去記事などをご参考ください。 (過去参考記事) webpackを使うVue.js 3系とTypeScriptの環境構築メモ webpack + TypeScript/Babel(JavaScript)の環境でReactを導入する   今回はこのようなファイル構成になります(環境構築用のファイルは省略しています)ビルド後のHTMLは共通で使います。ソースファイルはVue.jsとReactで分かれており、コンポーネントも同じ構成になるようにしています。ファイル名も同じにしているのでそれぞれ対応しているのが確認できますね。ただ、ReactではCSS Modulesという手法でスタイルを指定しているため、コンポーネントに紐づくCSSファイルが追加されています。(CSS Modulesについては別記事でまとめたいと思います)
dist
  ┣ index.html
  ┗ js
    ┗ main.js
src
  ┣ main.js
  ┣ vue
    ┣ App.vue
    ┗ components
      ┗ TaskItem.vue
  ┗ jsx
    ┣ App.jsx
    ┣ App.module.css
    ┗ components
      ┣ TaskItem.jsx
      ┗ TaskItem.module.css
  HTMLはこのように、body直下にマウント用の要素を用意しています。この要素の中にアプリ部分のDOMが展開される形になります。ここまではVue.jsもReactも同じになります。 【index.html】
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Sample APP</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./js/main.js"></script>
  </body>
</html>
   
Vue.jsとReactでそれぞれの実装方法とソースコードの比較
それでは実際にソースコードをそれぞれ見ていきます。まずはVue.jsの方から、エントリーポイントとなるJSファイルでモジュールやコンポーネントを読み込んでDOMにマウントしていきます。 【main.js(Vue.js)】
import { createApp } from 'vue';
import App from './vue/App.vue';

const app = createApp(App);
app.mount('#app');
  続けて親コンポーネントになります。Vue.jsでは拡張子を「.vue」にすることでHTMLとJavaScript、CSSを一つのファイルにまとめられる「単一ファイルコンポーネント」と呼ばれる形で扱うことができます。こうすることでスタイルや処理を持たせた各種コンポーネントを作成できます。 【App.vue(Vue.js)】
<template>
  <h1>TODO List(Vue)</h1>
  <ul class="list">
    <task-item v-for="(task, index) in tasks" :key="`${task}-${index}`" :task-data="{ index, task }" @delete-task="deleteTask" />
  </ul>
  <input v-model="newTask" class="task-input" type="text" placeholder="タスクを入力" />
  <button class="button-add" @click="addTask">追加</button>
  <div v-if="isError" class="error-message">タスクが空欄です</div>
</template>

<script>
  import TaskItem from './components/TaskItem.vue';
  export default {
    components: {
      TaskItem
    },
    data() {
      return {
        tasks: [],
        newTask: null,
        isError: false
      }
    },
    created() {
      this.fetchData();
    },
    methods: {
      fetchData() {
        this.tasks = [ 'タスク1', 'タスク2', 'タスク3' ];
      },
      addTask() {
        if(this.newTask !== null) {
          this.tasks.push(this.newTask);
          this.newTask = null;
          this.isError = false;
        } else {
          this.isError = true;
        }
      },
      deleteTask(payload) {
        this.tasks.splice(payload, 1);
      }
    }
  }
</script>

<style scoped>
  .list {
    list-style-type: none;
    margin: 0;
    padding: 0;
  }
  .task-input {
    margin: 20px 20px 0 0;
  }
  .error-message {
    color: red;
  }
</style>
  Vue.jsではこのようにテンプレート(HTML)、処理(JavaScript)、スタイル(CSS)がそれぞれ独立しており、とてもわかりやすくなっているので習得するまでの学習コストが低いとされています。直感的に書けるものの、コンポーネントが複雑化するとコードも肥大するというデメリットもあります。続けて子コンポーネントも同じくみていきます。こちらも親コンポーネント同様に単一ファイルコンポーネントの形になります。 【TaskItem.vue(Vue.js)】
<template>
  <li class="list-item">
    <button class="button-delete" @click="emitDeleteTask(taskData.index)">削除</button>
    <span>{{ taskData.task }}</span>
  </li>
</template>

<script>
  export default {
    name: 'TaskItem',
    props: {
      taskData: {
        type: () => {}
      }
    },
    methods: {
      emitDeleteTask(index) {
        this.$emit('delete-task', index)
      }
    }
  }
</script>

<style scoped>
  .list-item:not(:first-child) {
    margin: 10px 0 0 0;
  }
  .button-delete {
    margin: 0 10px 0 0;
  }
</style>
  子コンポーネント側では親コンポーネントからPropsで値を受け取ってコンポーネント内で展開しています。このようにVue.jsではコンポーネントも比較的直感でわかりやすい構造になっています。ただ、Options APIの場合には独自の書き方になっているものが多いため、これらをまずは覚える必要があります。(参考記事:Vue.jsの2系でTypeScriptを使う「Options API」と「Class API」)   では次はReactのソースコードをみていきます。作成するアプリは同じ機能で同じ見た目になるのですが、コードの書き方など実装方法が大きく異なる部分があります。まずはエントリーポイントのJSファイルでDOMにマウントしていきます。 【main.js(React)】
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './jsx/App.jsx';

const Root = () => {
  return (
    <>
      <App />
    </>
  )
};
ReactDOM.render( <Root />, document.getElementById('app') );
  そして親コンポーネントをみていきます。こちらも同じく関数コンポーネントで作成していきます。 【App.jsx(React)】
import React, { useState, useEffect, useRef } from 'react';
import { TaskItem } from './components/TaskItem.jsx';
import AppCSS from './App.module.css';

export const App = () => {
  const [tasks, setTasks] = useState([]);
  const [newTask, setNewTask] = useState(null);
  const [isError, setError] = useState(false);
  const fetchData = () => {
    setTasks([ 'タスク1', 'タスク2', 'タスク3' ]);
  };
  const dataBinding = (event) => {
    setNewTask(event.target.value)
  };
  const refs = useRef(null);
  const formReset = () => {
    refs.current.value = null
  };
  const addTask = () => {
    if(newTask !== null) {
      setTasks([...tasks, newTask]);
      setNewTask(null);
      setError(false);
    } else {
      setError(true);
    }
  };
  const deleteTask = (payload) => {
    setTasks(
      tasks.filter((task, index) => index !== payload)
    )
  };
  useEffect(() => {
    fetchData();
  }, [])
  return (
    <>
      <h1>TODO List(React)</h1>
      <ul className={AppCSS['list']}>
      {
        tasks.map((task, index) => {
          return <TaskItem 
            taskData={{ index, task }} 
            key={`${task}-${index.toString()}`} 
            methods={
              { deleteTask }
            } 
          />
        })
      }
      </ul>
      <input 
        ref={refs}
        onChange={dataBinding}
        className={AppCSS['task-input']} 
        type="text" 
        placeholder="タスクを入力" 
      />
      <button 
        onClick={() => {
          addTask();
          formReset();
        }}
        className={AppCSS['button-add']}
      >追加</button>
      {
        isError && <div className={AppCSS['error-message']}>タスクが空欄です</div>
      }
    </>
  )
};
  また、Vue.jsとは異なりReactでコンポーネント内でCSSを扱う方法はいくつかあります。今回は「CSS Modules」という手法でスタイルを設定しています。JSX内のclassName属性で、読み込んだCSSのセレクタを指定することにより、class名がランダムな文字列に変換され、コンポーネント内をスコープとしたCSSを作成することができます。Vue.jsでいうscoped CSSに相当するものですね。ビルド後はコンポーネントをスコープとしたスタイル用にclassに変換されているのが確認できます。   ちなみにCSS Modulesとして読み込むCSSはこのように通常のCSSとして記述していきます。ReactではJavaScriptの中でCSSを扱う方法もありますが、こちらは慣れている人も多いかと思いますので、使いやすいのではないでしょうか。一方でコンポーネントとCSSファイルが対になるためコンポーネントの数が多くなった場合には保守性にやや欠ける印象はあります。 【CSS Modules(React)】
// App.module.css
.list {
  list-style-type: none;
  margin: 0;
  padding: 0;
}
.task-input {
  margin: 20px 20px 0 0;
}
.error-message {
  color: red;
}

// TaskItem.module.css
.list-item:not(:first-child) {
  margin: 10px 0 0 0;
}
.button-delete {
  margin: 0 10px 0 0;
}
  続いて子コンポーネントもみていきます。こちらも親コンポーネントと同じく、関数コンポーネントでHTMLを出力していく形になります。親コンポーネントからPropsとして受け取った値は、コンポーネント用関数の引数でからアクセスができます。子コンポーネントで側でもCSS Modulesを採用しています。 【TaskItem.jsx(React)】
import React from 'react';
import TaskItemCSS from './TaskItem.module.css';

export const TaskItem = (props) => {
  return (
    <>
      <li className={TaskItemCSS['list-item']}>
        <button 
          onClick={() => props.methods.deleteTask(props.taskData.index)} 
          className={TaskItemCSS['button-delete']}
        >削除</button>
        <span>{props.taskData.task}</span>
      </li>
    </>
  )
};
  実際にこれらで作成したアプリはこのような感じになります。ソースコードは異なりますが、見た目や機能は全く同じものができました。Vue.jsとReactはこのように実装方法が異なりますが、それぞれのメリットや特徴を活かしながら使い分けるのが良いですね。それでは実際に細かい違いを比べていきたいと思います。    
1. DOMへのマウント
Vue.jsやReactによって動的に生成されるDOMは、静的HTMLの要素にマウントさせる必要があります。各種モジュールをインポートし、対象の要素を取得してマウントするのですが、Vue.js(3系の場合)は「createApp()」でインスタンスを作成し、「mount()」で指定の要素にマウントしていきます。Reactの17系以下では「ReactDOM.render()」で、18系以降では「createRoot().render()」でマウントさせます。Reactでは関数コンポーネントの場合、関数を直接レンダーするとエラーになるので、このようにマウント用のコンポーネントを定義して、その中でコンポーネントを呼び出す必要があります。この辺りが大きく異なる点ですね。
// Vue.js(3系)

import { createApp } from 'vue';
import App from './vue/app.vue';

const app = createApp(App);
app.mount('#app');

// React(17系)
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './jsx/App.jsx';

const Root = () => {
  return (
    <>
      <App />
    </>
  )
};
ReactDOM.render( <Root />, document.getElementById('app') );
   
2. コンポーネント
Vue.jsではコンポーネントを定義する際に.vue拡張子がついた単一ファイルコンポーネントを使う方法と、「Vue.createApp({}).component()」でコンポーネントを定義するパターンもあります。ここでは単一ファイルコンポーネントを使う方法で比較しています。「template」内にHTMLを書き、「script」内に各種メソッドや変数を配置します。「style」にはスタイルを指定しますが、「scoped」を付けることでスタイルのスコープを同一ファイル内に限定することができます。importで必要なライブラリやコンポーネントを読み込み、default exportで処理や変数を扱っていきます。
// Vue.js
<template>
  ....
  <child-component />
  ....
</template>

<script>
  import ChildComponent from './PATH/ChildComponent.vue';
  export default {
    name: 'App',
    components: {
      ChildComponent
    },
    ....
  }
</script>

<style scoped>
  .selector {
    ....
  }
</style>
  Reactではコンポーネントを定義する際に、クラスで定義するパターンと関数で定義するパターンがあります。ここでは後者の関数でコンポーネントを定義する形でみていきます。関数内にコンポーネント内で使われる変数や処理を定義し、JSX記法でHTMLを返すように書いていきます。(参考記事:create-react-appでReactの導入とJSXの記法に触れてみる)importで必要なライブラリやコンポーネントを読み込み、関数としてexportしていきます。
// React
import React, { useState, useEffect, useRef } from 'react';
import { ChildComponent } from './components/ChildComponent.jsx';
import AppCSS from './App.module.css';

export const App = () => {
  ....
  return (
    <>
      ....
      <ChildComponent />
      ....
    </>
  )
};

---------- CSS Modules ----------
.selector {
  ....
}
  CSSは外部ファイルとして用意します。ここで採用されているCSS Modulesではコンポーネントファイル内でimportによってCSSを読み込みます。そのため、一般的にはコンポーネントに対応した専用のCSSファイルを用意し、webpackなどを使ってバンドルしていきます。詳しくはこちらの記事(webpackでSassのコンパイル環境の作成と外部CSSへの書き出しをできるようにする)などをご参考ください。  
  フロントエンドのフレームワークとして比較されることの多い、Vue.jsとReactですがこのように実装方法に違いがあるので、移行する際にはそれぞれの特徴を覚えておきたいですね。次回記事ではコンポーネント内で扱うメソッドや変数の扱いについて違いをまとめてみたいと思います。
  • はてなブックマーク
  • Pocket
  • Linkedin
  • Feedly

この記事を書いた人

Twitter

sponserd

    keyword search

    recent posts

    • Twitter
    • Github
    contact usscroll to top
      • Facebook
      • Twitter
      • Github
      • Instagram