見出し画像

Vue3のComposition APIをつかって既存のVueコンポーネントを書き換えてみた

最近Vue2で作り始めた自分のサイドプロジェクトを途中でVue3にVue.jsのバージョンを上げました。今までの書き方(Options API)でも動くもののVue3のサンプルコードを見ていると新しくVue3で標準となったComposition APIに準拠した形で書かれているものが多く、この機会に自分もコンポーネントをCompotion APIに即したものに書き換えてみました。この際どのように既存のコードを変えていったかを、Composition APIに既存のVueプロジェクトをマイグレーションする時の参考になればと思って記録してみました。


サンプル

位置情報のリストを表示するページ用の下のようなVueコンポーネントを今回Composition APIを使って書き換えてみました。

<script>
export default {
  name: 'LatLng',

  data() {
    return {
      latLngs: [],
      offset: 0,
      limit: 5,
      totalCount: 100,
      currentLatLng: null,
      filter: {
        age: true,
        hAcc: true,
        vAcc: true,
      },
    };
  },

  computed: {
    isFirstPage() { ... },
    isLastPage() { ... },
        latLngCreatedDatetime: function () { ... },
        ...
    ...mapState(['path']),
    ...mapState(['project']),
  },

  methods: {    
    async fetchNextLatLngs() {
      ...
      this.fetchLatLngs();
    },

    async fetchPreviousLatLngs() {
      ...
      this.fetchLatLngs();
    },

    async fetchLatLngs() { ... },

    rowClicked(latLng) { ... },
  },

  mounted() {
    ...
    this.fetchLatLngs();
  },
};
</script>

data()の移動

data()の中は、下のようになっていたのですが、

data() {
    return {
      latLngs: [],
      offset: 0,
      limit: 5,
      totalCount: 100,
      currentLatLng: null,
      filter: {
        age: true,
        hAcc: true,
        vAcc: true,
      },
    };
  },

これが下のようになりました。

setup() {   
    const offset = ref(0);
    const limit = 5;
    const totalCount = ref(0);
    const currentLatLng = ref(null);
    const filter = reactive({
      age: true,
      hAcc: true,
      vAcc: true,
    });

リアクティブにしたい変数の移動

Vue.jsではdata()に定義した変数は、全てリアクティブになります。リアクティブとはどういうことかというと、「変数の値の変更を常にモニタしてくれて、変更があったらその変数の使われている(表示されている)UIの箇所を再度、描画し直してくれる」ということです。

offset: 0,
totalCount: 100,

Options API (旧フレームワーク)では、data()とcomputedに記載したものはすべてリアクティブになっていました。これがVue3のComposition APIから、明示的にリアクティブの変数として宣言したものだけが、リアクティブになります。

下のように、ref()で初期値を囲んで宣言することで、リアクティブな変数を宣言できます。

const latLngs = ref([]);
const offset = ref(0);
const totalCount = ref(0);

RxSwiftやRxKotlinなど、他の言語でリアクティブプログラミングに慣れている方は、Observableでオブジェクトを初期化するのと、考え方・記法ともによく似ているのでわかりやすいかと思います。(私はまだ使ったことがありませんでしたが、Vueにも2.6からVue.observableが導入されています。)

refという言葉が今までの$refと関係があるのかと思って私は混乱しましたが、$refとは全く関係ありません

data()からこのように、変数ごとにref()で初期化するように変えた理由は、パフォーマンス的なメリットではないかと私は考えています。data()にあるものの中には他の関数で使うだけで、UIから参照されないものもあります。これらのデータの変更まで全部モニタしているのは、ブラウザのリソース(そしてブラウザを動かしているコンピューターのリソース)がもったいないので、このように必要なものだけをリアクティブにするという意図ではないかと思います。

ちなみにリアクティブ = Reactive = Re ・Activeということで、再びアクティブになる、ということだと私は理解しています。ただの変数は一度初期の値をUIで表示してしまうと、その後その値を変えてもUI上ではアクティブになりません、それを再度アクティブにする=>再度値を読み込んでくれる=値が変わったことで、関係しているもの全部が再度アクティブになる、という意味でリアクティブという言葉が使われていると私は思っています。


.valueを介してデータにアクセス

refで初期化した変数にはvalueを使って中身にアクセスします。

offset.value = offset.value + limit

コードのなかからはこのようにvalueを使って値を取り出したり、セットしたりしないといけないのですが、templateからアクセスするときはこのvalueを記述する必要がなくなります。

UI (template)からは、valueを記述しなくていい

<p>{{ offset }}から{{ limit }}を表示中</p> 
templateのマークアップが見やすくな

テンプレートのマークアップが読みやすいまま維持されるのでこれは嬉しい仕様だと思いました。

リアクティブにしたくない変数の移動

一方、UIで表示していなくて、リアクティブにする必要のない変数は下のように普通の宣言にします。

今回の場合では、一度にダウンロードしてくる位置情報(LatLng)のリストの数を格納しておくlimitは値が変わることがなく、ダウンロードしてくる関数内でのみ使われているので、setup()のなかで今までのように普通に宣言して初期値を代入します。

const limit = 5;

このように、リアクティブにしなくていいものとしたいものを分けて管理してパフォーマンスを向上することがComposition APIのメリットだと思いました。

リアクティブにしたいオブジェクトの移動

リアクティブにしたいもののうち、integer, boolean, stringなどのプリマティブなものはすべてrefで初期化し、オブジェクトはreativeを使って初期化するという方法が公式ドキュメントなどで奨励されているのを見ました(2021年12月現在)。
ですので、もともとは下のようになっていたものを、

data() {
    return {   
      latLngs: [],   
      currentLatLng: null,
      filter: {
        age: true,
        hAcc: true,
        vAcc: true,
      },

setup()では下のように宣言しました。

setup() {   
    const latLngs = ref([]);
    const currentLatLng = ref(null);    
    const filter = reactive({
      age: true,
      hAcc: true,
      vAcc: true,
    });

ここで、latLngsとcurrentLatLngはreactiveにしませんでした。(理由は後で説明します)

3種類のフィルタの値を格納するfilterというオブジェクトだけ、reactiveにしました。

reactiveは、refと違って、.valueを介さずに、変数の中身にアクセスできます。

filter.hAcc = 30

のように、.valueを介さず、値の設定ができます。filter自体はリアクティブになっているので、このように中身のhAccを変更すると、UI上で変更が反映されます。

もちろん読み取りも.valueを介さずに読めます

    console.log('filter value is', filter.hAcc)

リアクティブにしたいオブジェクトのうちref()で初期化したもの

こちらの2つのオブジェクトはreactiveではなく、ref()で初期化しました。

const latLngs = ref([]);
const currentLatLng = ref(null);    

1. 配列はreactiveを使って初期化できない

いろいろと実験してわかったのですが、配列はreactiveをつかって宣言できませんでした。

以下のVue.jsのGitHubのIssuesでなぜできなんだ!と怒っている人がいますが、今はできないようです。

2. reactiveで初期化したオブジェクトの変数に別のオブジェクトを挿入すると、その変数はreactiveではなくなる。

私も最初はcurrentLatLngを

const currentLatLng = reactive(null);

のように初期化してみました。

reactive()で初期化した変数には.valueを使わずに中身にアクセスできるのがreactiveのメリットですが、オブジェクトのメンバー変数ではなく、オブジェクト自体を入れ替える以下のようなコードは

currentLatLng = {name: 'new object'}

const変数へのデータの書き換えができないのでエラーがでます。 そこで、

let currentLatLng = reactive(null);

のように、変数をletにしてみました。

こうするsetup()外で

currentLatLng = {name: 'new object'}

のように代入はできますが、代入した途端、currentLatLngのリアクティビィティが失われ、UIの再描画は行われなくなってしまいました。ですので、変数自体にプログラムのなかから頻繁にオブジェクトを代入したい場合は、ref()で初期化し、変数のvalueにオブジェクトを代入する必要があります。 考えてみれば、変数に直接違うオブジェクトを代入すれば変数の指し示すポインター(メモリーアドレス)が変わってしまい、新しく代入されたオブジェクトはリアクティブで初期化されたものではないので当たり前のことなのですが、公式ドキュメントやいろいろなブログで「オブジェクトはreactive()で」と書いてあったので、できると思ってしまいました。 ちなみにreactive()で宣言した変数にsetup()外でオブジェクトを代入するときにしたのように再度reactive()でラップしたオブジェクトを入れてみましたが、UIの再描画は起きませんでした。

currentLatLng = reactive({name: 'new object'})

上のコードが実行された瞬間リアクティビィティが失われました。
おそらくsetup時に返されたリアクティブ変数のポインターのリストのようなものをVue.jsが管理していて、新しいオブジェクトを代入してそのアドレスを変えてしまうと、リアクティブな変数の管理化には置かれなくなってしまうのだと思いました。

ですので、

const currentLatLng = ref(null);   

が、この場合、正解になり、setup()外からこのオブジェクトを入れ替えたいときは、下のように記述します。

currentLatLng.value = {name: 'new object'}

ということで、2021年12月現在では

  1. Arrayはref()でラップして初期化する

  2. 変数にオブジェクトを再代入する場合は、オブジェクトであってもref()で初期化する

というのが私にとっての解となっています。
reactiveは、オブジェクトの中身を変えるときは使える。オブジェクト自体を変えるときは使えない。ということを頭においてプログラミングするといいと思います。
(もし、「間違っているよ!」ということであれば、コメントなどいただけると助かりますmm)

なぜconstなのか

下のようにref()やreactive()でラップした変数はconst変数です。

const totalCount = ref(0);

なぜ値を変更できないimmutableなconst変数でいいのでしょうか?

それは、ref()でラップされた”データ”のメモリ上での保存場所と、ref()でラップした結果のリアクティブオブジェクトのメモリ上の保存場所が違うからです。

totalCount.value = 10

のように、値を変更したと場合は、totalCount変数が指すリアクティブオブジェクトのメモリアドレスは変わらず、ラップしたデータの保存場所の値だけが0から10に変わります。

totalCount変数の指すメモリアドレスの中身はかわっていないので、constで定義して問題ありません。

computedの移動

computed: ブロックで定義してたcomputed変数も、同じくsetupの中に移動します。 computed変数は新たに1つずつcomputed()という関数を使って初期化する必要があります。

    const isFirstPage = computed(() => {
      if (offset.value == 0) {
        return true;
      } else {
        return false;
      }
    });

computed()の中身に下のように無名関数のブロックのようなブロックをつくりそこに中身を記述します。

    () => {
      
    }

パラメーターを受け取るタイプのcomputed変数は、したのように書きます。

    const latLngCreatedDatetime = computed(() => {
      return function (latLng) {
        return moment(latLng.createdAt).format('YYYY/MM/DD HH:mm');
      };
    });  

computed()の書式から下のように書けそうだと思ってやってみましたが、だめでした。

//このコードは動きません    
const latLngCreatedDatetime = computed((latLng) => {
  return moment(latLng.createdAt).format('YYYY/MM/DD HH:mm');
}); 

これらのcomputed変数もsetup()のreturnで返すことを忘れないようにしましょう。

return {
      ...
      isFirstPage,      
      latLngCreatedDatetime,
    };
  },

methodsの移動

methodsはすべてアロー関数の形にして、setup()の中に移動します。下は通常の関数、async関数、引数を受け取る関数の例です。


const fetchPreviousLatLngs = () => {
      
};   
   
const fetchNextLatLngs = async () => {
 
};

const rowClicked = (latLng) => {
      
};

これも、setup()のreturnでこれらの関数ポインタを返すことを忘れないようにしましょう。

    return {
      ...
      fetchPreviousLatLngs,
      fetchNextLatLngs,
      rowClicked,      
    };

(元Cプログラマーにとっては関数のアドレスを格納したこれらの変数を関数ポインタと呼ぶとわかりやすいのですが、Javascript的には一般的ではないかもしれません。「関数を格納した変数」というほうが混乱がないかもしれません。)

Vuex (Store)の書き換え

Composition APIでは、Vuexへのアクセスを簡単にするmapHelpers(...mapState(), ...mapGetters, ...mapActions)は使えなくなりました。代わりにsetupのなかでuseStoreという関数をつかってstoreオブジェクトを取り出して、それをつかってstateやgettersにアクセスするcomputed関数を定義するということが必要になります。 まず、useStore関数をimportします。

import { useStore } from 'vuex';

その後この関数を使って、setup()の中で、storeオブジェクトを取り出します。

setup() {
    const store = useStore();

同じくsetup()のなかで、下のように

setup() {
    const store = useStore();
    const projects = computed(() => store.state.project.projects);

computed変数を定義して、そのなかで、storeの中のstateにアクセスします。

これで、projectsというcomputed変数を介してstoreのprojectモジュールにあるprojectsというstateにアクセスできるようになりました。

getterに関しても同じ要領で下のようにcomputed関数に1つずつマップします。

const projects = computed(() => store.getters.project.getProjects();

ライフサイクルフックの移動

mountedなどのライフサイクルフックもsetupの中に移動する必要があります。こちらの公式ドキュメントを参考にしました。


mountedは下のように、onMounted()をsetupの中でオーバーライドし、そのなかにmountedでやっていたことを記述します。

setup() {
    ...
    onMounted(() => {      
      let user = store.getters.user;
      if (!user) {
        fetchLatLngs();
      } else {
        router.push('/signin');
      }
    });

onMounted()の中では、setup()内に定義した他の変数や関数にアクセスができます。 注意点として、thisはsetup()の中のこの時点で存在していないので、this.fetchLatLngs()のようにthisを使うことはできません。

mounted以外の他のライフサイクルフックは以下のような名前の関数に変わりました。

ここで、beforeCreateとcreatedがなくなっていることに気づいた方がいると思いますが、公式ドキュメントには以下のようにかかれています。

💡 Because setup is run around the beforeCreate and created lifecycle hooks, you do not need to explicitly define them. In other words, any code that would be written inside those hooks should be written directly in the setup function.

https://v3.vuejs.org/guide/composition-api-lifecycle-hooks.html

setupが以前のbeforeCreateとcreatedが呼ばれるのと同じらへんのタイミングでよばれるので、beforeCreateとcreatedに書いていたコードは、setupのなかに書いてくれ、
と言っています。

上のonMounted()の例では、routerにアクセスして、ユーザーが未ログインだったらログインページに飛ばす、という処理をしていますが、CompositionAPIでのRouterの使い方を次に見ていきたいと思います。

Router

もともとthis.$routerでアクセスできていた$routerにアクセスする方法がCompostion APIでは変わりました。

import { useRouter } from 'vue-router';

のようにuseRouterをimportし、

setup() {
    const router = useRouter();

のように、routerオブジェクトを取得します。 これで、onMountedのなかで、下のように、routerにpushできるようになります。

setup() {
    const router = useRouter();

    onMounted(() => {      
      let user = store.getters.user;
      if (!user) {
        fetchLatLngs();
      } else {
        router.push('/signin');
      }
    });

(まだ試していませんが、setup()のreturnでrouterを返せば、setup()外からrouterオブジェクトの機能が使えると思います。)

ついでに、同じように使い方が変わった$routeについても紹介したいと思います。

Route

Vue2では

this.$route.query.email
this.$route.fullPath

というふうに書いていた$routeへのアクセスは、 Vue3では、

import { useRoute } from 'vue-router'

のようにuserRoute (userRouterではない)をimportした後、

setup() {
    const route = useRoute();
    
    const { email } = route.query;
    const { id } = route.params;

    const fullPath = route.fullPath;

のように使います。

まとめ

最終的に私のLatLngコンポーネントは下のようになりました。

export default {
  name: 'LatLng',

  setup() {
    const store = useStore();
    const router = useRouter();

    const latLngs = ref([]);
    const offset = ref(0);
    const limit = 5;
    const totalCount = ref(0);
    const currentLatLng = reactive({});
    const filter = reactive({
      age: true,
      hAcc: true,
      vAcc: true,
    });

    const isFirstPage = computed(() => {...});
    const isLastPage = computed(() => {...});    
    const latLngCreatedDatetime = computed(() => {...});
    
    const path = computed(() => store.state.path.path);
    const projects = computed(() => store.state.project.projects);

    const fetchNextLatLngs = async () => {
      ...
      fetchLatLngs();
    };

    const fetchPreviousLatLngs = async () => {
      ...
      fetchLatLngs();
    };

    const fetchLatLngs = async () => {...};

    const rowClicked = (latLng) => {...};

    onMounted(() => {
      fetchLatLngs();
    });

    return {
      //Variables
      latLngs,
      offset,
      limit,
      totalCount,
      currentLatLng,
      filter,
      
      //Computed
      isFirstPage,
      isLastPage,      
      latLngCreatedDatetime,
      
      //Store
      path,
      projects,

      //Methods
      fetchNextLatLngs,
      fetchPreviousLatLngs,
      fetchLatLngs,
      rowClicked,      
    };
  },
};
</script>

これを見てみると、すべてのコードがsetup()の中に収まっているのがわかります。

今回は、Vue2のときに作ったVueコンポーネントをVue3のComposition APIフレームワークに沿って書き換えた時に必要だった最小限のステップを記録し、参考までに共有させていただきました。
まだ私も経験が浅く、認識が間違っている部分もあるかもしれません、これから試行錯誤して行きながら間違いや新しいことを発見したらこの記事を更新する形で共有できたらと思っています。
御清覧ありがとうございました。

おまけ


公式ドキュメントの下の章では、

you might be already asking the question – Isn’t this just moving the code to the setup option and making it extremely big? Well, that’s true.

のように「けっきょくsetup()関数がただでかくなっただけじゃないかと思われていますよね?、確かにそのとおりですよ」のような少し自虐的に書かれた文章があります。
この文章の下に、setup()のなかでやっていることをComposition Functionという単位に分割して再利用する例が書かれています。
このやり方だとsetup()の中がすごくスッキリし、またやっていることも抽象化できると思いました。
Composition Functionに機能を分割していく方法も今後試してみたいと思っています。

追記

第2回↓を公開しました。合わせて参考にしていただけたらと思います。

追記(2)

Vue3.2から<script>タグを<script setup>とすることで宣言的に完結にVue3のComposition APIに即したコンポーネントが書けるようになりました↓↓↓



最後までお読みいただき、心から感謝申し上げます。この記事がお役に立ち、楽しんでいただけたなら、ぜひハートマークをクリックしていただくか、今後もこのようなWeb技術関連の記事に興味がある方は、アカウントのフォローをご検討ください。
皆様のサポートが私の大きな励みとなります!今後も有益な技術関連の情報を提供できるようエンジニア生活に勤しんでまいります!


このブログに関する質問やWebアプリ、iOS・Androidアプリの開発の相談はこちらから↓↓↓

@mizutory
mizutori@goldrushcomputing.com





この記事が気に入ったらサポートをしてみませんか?