Vue 3 Composition API 書き方メモ

Vue 3 Composition API 書き方メモ。
(2020/03/22 一部修正)

参考

環境準備

今回は、viteで初期環境を用意。

1
npm init @vitejs/app app --template vue

限りなく最小構成

<span>0</span>と出るだけ。
この状態のnumは、リアクティブではない。

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<span>{{num}}</span>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
setup: () => {
return { num: 0 };
},
});
</script>

props と context を受け取る

表示は、以下のようになる。

1
2
3
4
<div>
<span>0</span>
<span>ABC</span>
</div>

props と context を受け取るときは、以下のようになる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div>
<span>{{num}}</span>
<span>{{param1}}</span>
</div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
props: {
param1: {
type: String,
default: "ABC",
},
param2: {
type: Object,
default: () => ({}),
},
},
setup: (props, context) => {
// 以下の方法で props の中身にアクセス可能
// props.param1
// props.param2

// context は、
// { attrs, slots, emit, expose }
// でも可

return { num: 0 };
},
});
</script>

ref と reactive

リアクティブなオブジェクトの作成には、ref reactive toRef readonlyを使う。
(他に、shallowReactive shallowReadonlyなどあるが、当面使わなそう。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<template>
<div>
<!-- val1 val4 は書き換わらない -->
<div>{{ val1 }}</div>
<div>{{ val2 }}</div>
<div>{{ val3.p1 }}</div>
<div>{{ val4 }}</div>
<div>{{ val5 }}</div>
<div>{{ val6.p1.value }}</div>
<div>{{ val7.p1 }}</div>
</div>
</template>

<script lang="ts">
import { defineComponent, reactive, readonly, ref, toRef, toRefs } from "vue";

export default defineComponent({
setup: () => {
// リアクティブではない
let val1 = 0;

// リアクティブ
let val2 = ref(0);

// リアクティブ
let val3 = reactive({ p1: 0 });

// リアクティブではない
let tmp4 = reactive({ p1: 0 });
let val4 = tmp4.p1;

// リアクティブ
let tmp5 = reactive({ p1: 0 });
let val5 = toRef(tmp5, "p1");

// リアクティブ
let tmp6 = reactive({ p1: 0 });
let val6 = toRefs(tmp6);

let tmp7 = reactive({ p1: 0 });
const val7 = readonly(tmp7);

setInterval(() => {
// toRef toRefsで作ったものは .value が必要
val1++;
val2.value++;
val3.p1++;
val4++;
val5.value++;
val6.p1.value++;
// readonly を使うと直接書き換えできなくなる
// val7.p1++
// 元のオブジェクトで書き換える
tmp7.p1++;
}, 1000);

return { val1, val2, val3, val4, val5, val6, val7 };
},
});
</script>

shallowRef triggerRef がちょっと面白い。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div>
<!-- 値は 1 秒ごとに書き換わるが、 反映は5秒ごと-->
<div>{{ val1.p1 }}</div>
</div>
</template>

<script lang="ts">
import { defineComponent, shallowRef, triggerRef } from "vue";

export default defineComponent({
setup: () => {
const val1 = shallowRef({ p1: 0 });

setInterval(() => {
// この p1 は、shallowRef で作られたことにより、リアクティブではない。
val1.value.p1++;
}, 1000);

setInterval(() => {
// triggerRef を実行すると、shallowRef で作られたオブジェクトの変更を反映できる
triggerRef(val1);
}, 5000);

return { val1 };
},
});
</script>

computed

よく使うcomputed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div>
<div>{{ val1 }}</div>
<div>{{ val2 }}</div>
</div>
</template>

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

export default defineComponent({
setup: () => {
// ref で作ったオブジェクトで computed
const tmp1 = ref(0);
const val1 = computed(() => tmp1.value * 10);

// reactive で作ったオブジェクトで computed
const tmp2 = reactive({ p1: 0 });
const val2 = computed(() => tmp2.p1 * 100);

setInterval(() => {
tmp1.value++;
tmp2.p1++;
}, 1000);

return { val1, val2 };
},
});
</script>

watch と watchEffect

watch の方が使いがちの気がする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
<div>
<div>{{ val1 }}</div>
<div>{{ val2.p1 }}</div>
<div>{{ val3 }}</div>
<div>{{ val4.p1 }}</div>
</div>
</template>

<script lang="ts">
import { defineComponent, reactive, ref, watch, watchEffect } from "vue";

export default defineComponent({
setup: () => {
// ref で作ったオブジェクトで watchEffect
const val1 = ref(0);
watchEffect(() => console.log(`val1:${val1.value}`));

// reactive で作ったオブジェクトで watchEffect
const val2 = reactive({ p1: 0 });
watchEffect(() => console.log(`val2.p1:${val2.p1}`));

// ref で作ったオブジェクトで watch
const val3 = ref(0);
watch(val3, () => console.log(`val3:${val3.value}`));

// reactive で作ったオブジェクトで watch
const val4 = reactive({ p1: 0 });
watch(val4, () => console.log(`val4.p1:${val4.p1}`));
// もしくは
// 特定のプロパティだけ監視
// watch(() => val4.p1, ()=> console.log(`val4.p1:${val4.p1}`))
//
// 特定のプロパティを別のオブジェクトで取り扱う
// watch(() => val4.p1, (p = val4.p1)=> console.log(`val4.p1:${p}`))

setInterval(() => {
val1.value++;
val2.p1++;
val3.value++;
val4.p1++;
}, 1000);

return { val1, val2, val3, val4 };
},
});
</script>

Computed and watch | Vue.jsに watchEffect と watch の比較 が記載されている。

Perform the side effect lazily;
Be more specific about what state should trigger the watcher to re-run;
Access both the previous and current value of the watched state

  • watch は、Lazy に実行
  • watchEffect より watch の方が明確に監視対象を指定できる
  • 監視していた値の前後の値を参照できる

ライフサイクルフック

以前まで書いていた mounted などは、onMounted などのon~~に変わった。
setup()の中で呼び出す。

1
2
3
4
5
6
7
8
9
10
11
<template> </template>

<script lang="ts">
import { defineComponent, onMounted } from "vue";

export default defineComponent({
setup: () => {
onMounted(() => console.log("Execute onMounted"));
},
});
</script>

Provide と Inject

公式の composition API のところを見ると、provide したコンポーネントでも inject して使えそうな書き方をしているように見える。
がしかし、Provide / inject | Vue.jsを見ると、ちゃんと親子関係の説明をしていた。

正直これだと規模によるだろうが、ほとんど root コンポーネントで provide するんじゃないかとという感想。

シンプルに確認

親コンポーネントは以下のようになる。

src/App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div>
<Cmp1 />
<Cmp2 />
</div>
</template>

<script lang="ts">
import { defineComponent, provide, ref } from "vue";
import Cmp1 from "./components/Cmp1.vue";
import Cmp2 from "./components/Cmp2.vue";

export default defineComponent({
components: {
Cmp1,
Cmp2,
},
setup: () => {
// とりあえず適当に値を登録
// provide の第一引数は、サンプルだと
// const key: InjectionKey<string> = Symbol()
// となっているが、文字列でも可
provide("key", ref(0));
},
});
</script>

子コンポーネント[更新側]は以下のようになる。

src/components/Com1.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>{{val}}</div>
</template>

<script lang="ts">
import { defineComponent, inject } from "vue";

export default defineComponent({
setup: () => {
let val = inject("key");

let count = 0;
setInterval(() => {
val.value++;
}, 1000);

return { val };
},
});
</script>

子コンポーネント[表示側]は以下のようになる。

src/components/Com2.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>{{val}}</div>
</template>

<script lang="ts">
import { defineComponent, inject } from "vue";

export default defineComponent({
setup: () => {
const val = inject("key");

return { val };
},
});
</script>

ルートコンポーネントで provide

最上位のコンポーネントで provide する。

main.ts
1
2
3
4
5
6
7
import { createApp, ref } from "vue";
import App from "./App.vue";

const app = createApp(App);
// .provide で provide できる。
app.provide("key", ref(0));
app.mount("#app");

provide する対象をちゃんと切り出してみる

サンプルの内容も準拠して、切り出してみます。

provide されるストア。

src/store.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { InjectionKey, reactive, provide, toRefs } from "vue";

// データの型定義
export interface StateDataInterface {
val1: number;
val2: number;
}

// キーを用意
export const storeKey: InjectionKey<StateDataInterface> = Symbol(
"storeDataKey"
);

// リアクティブなデータの登録
export const storeData = () => {
return reactive({ val1: 0, val2: 0 });
};

ストアを provide する。

main.ts
1
2
3
4
5
6
7
import { createApp, ref } from "vue";
import App from "./App.vue";
import { storeKey, storeData, StateDataInterface } from "./store";

const app = createApp(App);
app.provide<StateDataInterface>(storeKey, storeData());
app.mount("#app");

inject する側は、以下のようにする。

子コンポーネント[更新側]

src/components/Com1.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>{{val ? val.val1 : "" }}</div>
</template>

<script lang="ts">
import { defineComponent, inject } from "vue";
import { storeKey, StateDataInterface } from "./../store";

export default defineComponent({
setup: () => {
// そのまま store.ts で定義されている reactive のオブジェクトをそのまま取り出す
const val = inject<StateDataInterface>(storeKey);

setInterval(() => {
if (!val) return;
val.val1++;
}, 1000);

return { val: val };
},
});
</script>

子コンポーネント[表示側]

src/components/Com2.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>{{val}}</div>
</template>

<script lang="ts">
import { defineComponent, inject, toRefs } from "vue";
import { storeKey, StateDataInterface } from "./../store";

export default defineComponent({
setup: () => {
// toRefs で 個別に取り出す
// val1 と val2 を取り出せるが今回使うのはval1だけ
let { val1 } = toRefs(
inject<StateDataInterface>(storeKey, { val1: 1, val2: 2 })
);

return { val: val1 };
},
});
</script>

Vue Composition API 一通り基本的書き方一旦確認できたので良かったかなと。
ref と reactive 周りが、1 つ下のオブジェクトを別の変数に取り出したりしただけで意図しないことが起きるのは注意ですね。

ではでは。