【Vue3】リアクティブとは?refやcomputed(算出プロパティ)

本記事では、Vue.jsの重要な概念である、
リアクティブについて扱います。

その中でも、refやcomputedについて、
詳細に解説します。

動画でご覧になりたい方は、
こちらをご参考ください。

リアクティブとは?

リアクティブとは何でしょうか?
私も最初、この概念がよくわかりませんでした。

記事とかドキュメントを見ても、
いまいちピンと来ませんでした。

リアクティブとは、簡単に言うと、
変数の値が変化すると、view側もそれに応じて変わってくれる流れみたいないものです。

これは、双方向データバインディングを想起するとイメージしやすいと思いますが、
JS → HTML、HTML → JSのように、変更に応じて、
一方に反映させていくような感じですね。

大抵の場合、変数はJS側で定義し、
view側はHTMLになるので、
本質的には、双方向データバインディングの議論と
あまり変わらないかなと思います。
(双方向データバインディングについては、こちらの記事をご参考ください。)

具体的には、下記あたりがそれに該当しますかね。

あとは、JavaScriptプロキシとかも該当するのかもしれません
(混乱するだけだから、特に意識しなくてもいいような気もしますが...)

リアクティブの厳密な定義としては、
変更がview側に検知されて、view側を気にしなくていいメリットがあるみたいな感じだと思いますが、
Vue.jsにおけるリアクティブといったら、ほぼほぼ、
**ref()**のことを言っていると思っても大丈夫だと思います。

同じ種類として、reactive()というのもありますが、
ぶっちゃけ使う頻度は、ref()よりは下がると思います。

computed()も、リアクティブといえば、
リアクティブなのですが、
それよりは算出プロパティという、
関数としての特徴のほうが印象的な気がします。
(算出プロパティはリアクティブな依存関係にもとづきキャッシュされるので、
そういった意味では、リアクティブの関数verみたいな感じですが)

https://ja.vuejs.org/guide/essentials/computed.html

ref

さて、それでは主役のrefとは一体何でしょうか?

まず、リアクティブな変数を作成するには、
ref以外にも、reactive()というのもあります。

https://ja.vuejs.org/guide/essentials/reactivity-fundamentals.html

ただ、reactive()は、オブジェクト型にしか使えないため、
文字列とか数値みたいなプリミティブ型には使えないという制限があります。

プリミティブ型って何?という方は、
下記の記事をご参考ください。

[st-card myclass="st-no-shadow" id="23" label="参考" pc_height="" name="" bgcolor="#cccccc" color="#ffffff" webicon="st-svg-file-text-o" readmore="on" thumbnail="on" type="text"]

その制限に対処したのが、
ref()という感じです。

そのため、refでリアクティブな変数を作成し、
プリミティブ型にも対処できるという感じですね。

例えば、

<template>
  <div>
    <input type="text" ref="inputRef">
    <button @click="handleClick">ボタン</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const inputRef = ref<HTMLInputElement>();

function handleClick() {
  if (inputRef.value) {
    console.log(inputRef.value.value);
  }
}
</script>

こんな感じのコードがあるとすると、
inputRefという変数がリアクティブになっているので、
inputタグの中身が変われば、
変数の中身も変わっていき、ボタンをクリックすると、
その変わった変数の中身を出力しているといった感じですね。

基本的には、こんな感じで、
HTMLのフォームとかの値をTypeScriptの変数の方にも反映させたいときに、
refとか、v-modelを使用することになるでしょう。

リアクティブじゃないとどうなる?

リアクティブじゃないコードのサンプルを紹介します。

<template>
  <div>
    <p>商品価格: {{/* price */}}円</p>
    <p>消費税込み価格: {{/* priceIncludingTax */}}円</p>
    <button @click="handleClick">ボタン</button>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

let price = 1000

const priceIncludingTax = computed(() => {
  const taxRate = 0.1;
  return Math.round(price * (1 + taxRate));
});
function handleClick() {
  price += 100
  console.log(price)
}
</script>

この場合、Typescript側の変数priceの中身と、
html(view側)で表示されている値が食い違っています。

画面に描写されているのは、
初期値の、1000と1100ですが、
console.logでは、ちゃんと100づつ加算されている、
変数priceが出力されています。

リアクティブな場合と比べてみます。
※変数名が手抜きなのは許してください

<template>
  <div>
    <p>商品価格: {{/* price */}}円</p>
    <p>ref商品価格: {{/* refprice */}}円</p>
    <p>消費税込み価格: {{/* priceIncludingTax */}}円</p>
    <p>ref消費税込み価格: {{/* refpriceIncludingTax */}}円</p>
    <button @click="handleClick">ボタン</button>
    <button @click="refClick">リアクティブボタン</button>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

let price = 1000
const refprice = ref(1000)

const priceIncludingTax = computed(() => {
  const taxRate = 0.1;
  return Math.round(price * (1 + taxRate));
});

const refpriceIncludingTax = computed(() => {
  const taxRate = 0.1;
  return Math.round(refprice.value * (1 + taxRate));
});

function handleClick() {
  price += 100
  console.log(price)
}

function refClick() {
  refprice.value += 100
  console.log(refprice.value)
}
</script>

この場合、リアクティブの方は、
加算されるに従って、画面も反映されていますね。

computed(算出プロパティ)

上記で少し登場しましたが、
computedは、さっきのコードで言うと、

import { ref, computed } from 'vue';

const priceIncludingTax = computed(() => {
  const taxRate = 0.1;
  return Math.round(price * (1 + taxRate));
});

const refpriceIncludingTax = computed(() => {
  const taxRate = 0.1;
  return Math.round(refprice.value * (1 + taxRate));
});

ここの部分です。
Vue3 Composition APIの場合、importして使用します。

構文的には、computed()の中に、関数処理を書いていく感じです。
基本的には、複雑なロジックを関数として、
htmlの方に表示するために使われるので、
{{/**/}}構文の中に複雑なロジックを書かないための改善策みたいな感じです。

computed()は、普通の関数としても実装はできますが、
少し違いがあります。

<template>
  <div>
    <p>ref商品価格: {{/* refprice */}}円</p>
    <p>ref消費税込み価格: {{/* refpriceIncludingTax */}}円</p>
    <p>普通の関数: {{ func1() }}</p>
    <button @click="refClick">リアクティブボタン</button>
    <button @click="refClick2">リアクティブボタン22</button>
    <p>{{/* omake */}}</p>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

const refprice = ref(1000)
const omake = ref(10)

const func1 = () => {
  console.log('普通の関数の処理')
  const taxRate = 0.1;
  return Math.round(refprice.value * (1 + taxRate));
}

const refpriceIncludingTax = computed(() => {
  console.log('computedの処理')
  const taxRate = 0.1;
  return Math.round(refprice.value * (1 + taxRate));
});

function refClick() {
  refprice.value += 100
  console.log(refprice.value)
}

function refClick2() {
  omake.value += 10
}
</script>

computedは、中のリアクティブな変数の値が更新された際にだけ、
呼ばれるのに対し、普通の関数は、再描画が起きると毎回呼ばれます。

つまり、computedのほうが、リアクティブなキャッシュを保持しているといった感じです。

上記のコードの場合、
リアクティブボタンを押すと、
computedが感知している、リアクティブなrefprice関数が書き換わるので、
処理が呼ばれますが、自身が感知していない、omake変数がリアクティブ22ボタンで書き換わっても、
computedの方は呼ばれないといった感じです。

まあ、正直この違いをクリティカルに利用したいユースケースは、
そんなにない気がしますが...笑

computedと普通の関数の違いとしては、
このような感じです。