くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Vue3のReactivityを学び直したときのまとめ

Vue3でいろいろ試してるけど、若干混乱してきたので、
ガイドを再度読み直してみた。慣れるまでむずい。。

まとめ

  • リアクティブの基本
    • リアクティブにしてくれるのはref()。オブジェクトもdeepで追跡
    • reactive()はリンクを保ったまま、ラップ/アンラップをしてくれる便利関数/オブジェクト
    • reactiveオブジェクトから一部を取り出すときは、toRefs/toRefを使って、リンクを保つ
    • Refから値を取り出す便利関数unref()もある
  • 基本要素
    • computed ... Refを返す
    • useState ... Refを返す
    • props ... Refを返す
  • その他
    • composition関数からreactiveオブジェクトを返すとき ...toRefs()
    • propのrefをcomposition関数に渡したいときは ... toRef()
      • toRefsでは省略可能なpropsを取得できない

宣言

リアクティブな状態を作る方法は2つ

  • reactive() ... オブジェクト
  • ref() ... プリミティブ or オブジェクト

reactive()ref()もディープにリアクティブになる。
reactive()はアンラップした状態で利用できるため、通常のオブジェクトのように利用できる。

// reactive()
import { reactive } from 'vue'
const state = reactive({
  count: 0
})

// -----------

// ref()
import { ref } from 'vue'
const count = ref(0)

リアクティブオブジェクトへのアクセス

アンラップ

const countRef = ref(0)
const state = reactive({ count: countRef });

console.log(state.count) // 0

state.count = 1
console.log(countRef.value) // 1

reactiveを経由すると、内部のrefにアクセスできる。

refの再代入

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
console.log(countRef.value) // 1

しかし、再代入すると、refが置き換わるため、リンクが切れる。

ArrayやMapはアンラップされない

const books = reactive([ref('Vue 3 Guide')])
// ここでは .value が必要です
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// ここでは .value が必要です
console.log(map.get('count').value)

refのアンラップはリアクティブな Object の中の入れ子となっている場合にのみ発生します。
refがArrayやMapのようなネイティブのコレクション型からアクセスされた場合、アンラップは行われません

分割代入はリアクティブを失う

import { reactive } from 'vue'

const book = reactive({
  author: 'Vue Team',
  title: 'Vue 3 Guide',
})

// リアクティブではない
let { author, title } = book

// リアクティブを保持
let { author, title } = toRefs(book)

reactive()

オブジェクトのリアクティブなコピーを返します。
リアクティブの変換は「ディープ」で、ネストされたすべてのプロパティに影響します。

const obj = reactive({ count: 0 })

reactiveは、refのリアクティビティを維持しながら、全ての深さのrefをアンラップします。

const count = ref(1)
const obj = reactive({ count })

// ref はアンラップされる
console.log(obj.count === count.value) // true

// `obj.count` が更新される
count.value++
console.log(`${count.value}, ${obj.count}`) // 2, 2

// `count` の ref も更新される
obj.count++
console.log(`${count.value}, ${obj.count}`) // 3, 3

reactiveのプロパティにrefを代入すると、そのrefは自動的にアンラップされます。

const count = ref(1)
const obj = reactive({})

obj.count = count

console.log(obj.count) // 1
console.log(obj.count === count.value) // true

ref()

内部の値を受け取り、リアクティブでミュータブルなrefオブジェクトを返します。

const count = ref(0)
console.log(count.value) // 0

refの値としてオブジェクトが割り当てられている場合、
そのオブジェクトはreactive関数によってディープなリアクティブになります。

unref()

val = isRef(val) ? val.value : valのシュガー(簡易)関数です。

const count = ref(0);
console.log(count.value); // 0
console.log(unref(count)); // 0

toRef()

reactiveオブジェクトのプロパティに対するrefを作成するために使用できます。 このrefは、リアクティブな接続を維持したまま、引き渡すことができます。

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

toRefは、propのrefをcomposition関数に渡したいときに便利です

export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  }
}

toRefは、ソースとなるプロパティが現在存在しない場合でも、使用可能なrefを返します。
これはtoRefsで取得されない省略可能なpropsを扱うときに特に便利です

toRefs()

reactiveオブジェクトをプレーンオブジェクトに変換します。
変換後のオブジェクトの各プロパティは、元のオブジェクトの対応するプロパティを指すrefとなります。

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs の型:

{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// ref と元のプロパティは「リンク」している
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

toRefsは、composition関数からreactiveオブジェクトを返すときに便利で、
利用する側のコンポーネントリアクティビティを失うことなく、返されたオブジェクトを分割代入できます

function useFeatureX() {
  const state = reactive({ foo: 1, bar: 2 })
  
  // 状態で動作するロジック

  // 返すときに ref に変換する
  return toRefs(state)
}

export default {
  setup() {
    // リアクティビティを失うことなく分割代入できる
    const { foo, bar } = useFeatureX()
    return { foo, bar }
  }
}

toRefsはソースオブジェクトに含まれるプロパティのrefを生成するだけです。
特定のプロパティのリファレンスを作成するには、代わりにtoRefを使用してください。

useState

useState<T>(init?: () => T | Ref<T>): Ref<T>
useState<T>(key: string, init?: () => T | Ref<T>): Ref<T>

おまけ: 型定義

// ref
function ref<T>(value: T): Ref<UnwrapRef<T>>;

// reactive
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>;

// toRefs
function toRefs<T extends object>(object: T): ToRefs<T>;

// toRef
function toRef<T extends object, K extends keyof T>(object: T, key: K): ToRef<T[K]>;

// runref
function unref<T>(ref: T | Ref<T>): T;

// useState
function useState<T>(key: string, init?: () => T | Ref<T>): Ref<T>

// computed
function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>;
function computed<T>(options: WritableComputedOptions<T>): WritableComputedRef<T>;

// props
function defineProps<TypeProps>(): Readonly<TypeProps>;

// ************************************************************************

// types
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
type UnwrapRef<T> = 
  T extends ShallowRef<infer V> ? V : 
  T extends Ref<infer V> ? UnwrapRefSimple<V> : UnwrapRefSimple<T>;
type ShallowUnwrapRef<T> = {
  [K in keyof T]: T[K] extends Ref<infer V>
    ? V
    : T[K] extends Ref<infer V> | undefined
      ? unknown extends V
        ? undefined
        : V | undefined
      : T[K]
};
type UnwrapRefSimple<T> = T extends
  | Function
  | CollectionTypes
  | BaseTypes
  | Ref
  | RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
  | { [RawSymbol]?: true }
  ? T
  : T extends Array<any>
    ? { [K in keyof T]: UnwrapRefSimple<T[K]> }
    : T extends object & { [ShallowReactiveMarker]?: never }
      ? {
          [P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>
        }
      : T

type ToRefs<T = any> = { [K in keyof T]: ToRef<T[K]> };
type ToRef<T> = IfAny<T, Ref<T>, [T] extends [Ref] ? T : Ref<T>>;

interface ComputedRef<T = any> extends WritableComputedRef<T> {
  readonly value: T
  [ComputedRefSymbol]: true
};
interface WritableComputedRef<T> extends Ref<T> {
  readonly effect: ReactiveEffect<T>
}
type ComputedGetter<T> = (...args: any[]) => T;
type ComputedSetter<T> = (v: T) => void;
interface WritableComputedOptions<T> {
  get: ComputedGetter<T>
  set: ComputedSetter<T>
};