kahirokunn
twitter: https://twitter.com/kahirokunn
qiita: https://qiita.com/kahirokunn
概要
- Vue3から組み込まれる
- Vue2の現時点でも公式ライブラリを使えば使える
- rfc段階
- Vue.jsが抱える型問題の大部分を解決するapi
資料
useCounter
import { computed, reactive } from "@vue/composition-api";
export function useCounter(initial = 0) {
const state = reactive({ count: 0 });
return {
count: computed(() => state.count),
set: (count: number) => {
state.count = count
},
inc: () => {
state.count++;
},
dec: () => {
state.count--;
},
reset: () => {
state.count = initial
}
} as const;
}
import { useState } from 'react'
const add = value => prev => prev + value
const useCounter = (initial = 0) => {
const [count, set] = useState(initial)
return {
count,
set,
inc: (number = 1) => set(add(number)),
dec: (number = 1) => set(add(-number)),
reset: () => set(initial),
}
}
export default useCounter
Sample Repo
Vue composition apiを触った際に作成したシンプルなサンプルです
mountされたらイベントをアタッチして、マウント解除されるタイミングで解除してます。
x, yはsourceがマウスカーソルなので代入禁止の意味を込めてcomputedにしています
import {
reactive,
onMounted,
onUnmounted,
computed
} from "@vue/composition-api";
export function useMouse() {
const pos = reactive({ x: 0, y: 0 });
const update = (e: MouseEvent) => {
pos.x = e.pageX;
pos.y = e.pageY;
};
onMounted(() => {
window.addEventListener("mousemove", update);
});
onUnmounted(() => {
window.removeEventListener("mousemove", update);
});
return {
x: computed(() => pos.x),
y: computed(() => pos.y)
} as const;
}
Component Code
useMouseを利用したコンポーネントコードはこんな感じです。
コードが凄く読みやすいと思います。
<template>
<div>
<h2>mouse pointer</h2>
<div>x: {{ x }}</div>
<div>y: {{ y }}</div>
<button @click="setDummyPos">setDummyPos</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from "@vue/composition-api";
import { useMouse } from "@/hooks/useMouse";
export default defineComponent({
setup() {
const { x, y } = useMouse();
return {
x,
y,
};
}
});
</script>
再利用性
debug時にこんなhooksがあると便利だと思います。
こんな感じにも使える
import {
onBeforeMount,
onMounted,
onBeforeUnmount,
onUnmounted,
onActivated,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onUpdated
} from "@vue/composition-api";
export function useLifeCycleLog() {
onBeforeMount(() => console.log("onBeforeMount"));
onMounted(() => console.log("onMounted"));
onBeforeUnmount(() => console.log("onBeforeUnmount"));
onUnmounted(() => console.log("onUnmounted"));
onActivated(() => console.log("onActivated"));
onBeforeUpdate(() => console.log("onBeforeUpdate"));
onDeactivated(() => console.log("onDeactivated"));
onErrorCaptured(() => console.log("onErrorCaptured"));
onUpdated(() => console.log("onUpdated"));
}
vs mixin
Vuetifyのコードはmixinでコードの共通化と再利用を上手くやっています。
しかし、これは宣言的ではない為、使い方が直感的ではなく、読み解くのに体力が必要です。
composition apiの利点は独立したネームスペースと宣言的なプログラミングができるため、使う側のコードも読みやすく、ロジックがバラけない3点かと思います。
// Styles
import './VAlert.sass'
// Extensions
import VSheet from '../VSheet'
// Components
import VBtn from '../VBtn'
import VIcon from '../VIcon'
// Mixins
import Toggleable from '../../mixins/toggleable'
import Themeable from '../../mixins/themeable'
import Transitionable from '../../mixins/transitionable'
// Utilities
import mixins from '../../util/mixins'
import { breaking } from '../../util/console'
// Types
import { VNodeData } from 'vue'
import { VNode } from 'vue/types'
/* @vue/component */
export default mixins(
VSheet,
Toggleable,
Transitionable
).extend({
name: 'v-alert',
props: {
border: {
type: String,
validator (val: string) {
return [
'top',
'right',
'bottom',
'left',
].includes(val)
},
},
closeLabel: {
type: String,
default: '$vuetify.close',
},
coloredBorder: Boolean,
dense: Boolean,
dismissible: Boolean,
icon: {
default: '',
type: [Boolean, String],
validator (val: boolean | string) {
return typeof val === 'string' || val === false
},
},
outlined: Boolean,
prominent: Boolean,
text: Boolean,
type: {
type: String,
validator (val: string) {
return [
'info',
'error',
'success',
'warning',
].includes(val)
},
},
value: {
type: Boolean,
default: true,
},
},
computed: {
__cachedBorder (): VNode | null {
if (!this.border) return null
let data: VNodeData = {
staticClass: 'v-alert__border',
class: {
[`v-alert__border--${this.border}`]: true,
},
}
if (this.coloredBorder) {
data = this.setBackgroundColor(this.computedColor, data)
data.class['v-alert__border--has-color'] = true
}
return this.$createElement('div', data)
},
__cachedDismissible (): VNode | null {
if (!this.dismissible) return null
const color = this.iconColor
return this.$createElement(VBtn, {
staticClass: 'v-alert__dismissible',
props: {
color,
icon: true,
small: true,
},
attrs: {
'aria-label': this.$vuetify.lang.t(this.closeLabel),
},
on: {
click: () => (this.isActive = false),
},
}, [
this.$createElement(VIcon, {
props: { color },
}, '$cancel'),
])
},
__cachedIcon (): VNode | null {
if (!this.computedIcon) return null
return this.$createElement(VIcon, {
staticClass: 'v-alert__icon',
props: { color: this.iconColor },
}, this.computedIcon)
},
classes (): object {
const classes: Record<string, boolean> = {
...VSheet.options.computed.classes.call(this),
'v-alert--border': Boolean(this.border),
'v-alert--dense': this.dense,
'v-alert--outlined': this.outlined,
'v-alert--prominent': this.prominent,
'v-alert--text': this.text,
}
if (this.border) {
classes[`v-alert--border-${this.border}`] = true
}
return classes
},
computedColor (): string {
return this.color || this.type
},
computedIcon (): string | boolean {
if (this.icon === false) return false
if (typeof this.icon === 'string' && this.icon) return this.icon
if (!['error', 'info', 'success', 'warning'].includes(this.type)) return false
return `$${this.type}`
},
hasColoredIcon (): boolean {
return (
this.hasText ||
(Boolean(this.border) && this.coloredBorder)
)
},
hasText (): boolean {
return this.text || this.outlined
},
iconColor (): string | undefined {
return this.hasColoredIcon ? this.computedColor : undefined
},
isDark (): boolean {
if (
this.type &&
!this.coloredBorder &&
!this.outlined
) return true
return Themeable.options.computed.isDark.call(this)
},
},
created () {
/* istanbul ignore next */
if (this.$attrs.hasOwnProperty('outline')) {
breaking('outline', 'outlined', this)
}
},
methods: {
genWrapper (): VNode {
const children = [
this.$slots.prepend || this.__cachedIcon,
this.genContent(),
this.__cachedBorder,
this.$slots.append,
this.$scopedSlots.close
? this.$scopedSlots.close({ toggle: this.toggle })
: this.__cachedDismissible,
]
const data: VNodeData = {
staticClass: 'v-alert__wrapper',
}
return this.$createElement('div', data, children)
},
genContent (): VNode {
return this.$createElement('div', {
staticClass: 'v-alert__content',
}, this.$slots.default)
},
genAlert (): VNode {
let data: VNodeData = {
staticClass: 'v-alert',
attrs: {
role: 'alert',
},
class: this.classes,
style: this.styles,
directives: [{
name: 'show',
value: this.isActive,
}],
}
if (!this.coloredBorder) {
const setColor = this.hasText ? this.setTextColor : this.setBackgroundColor
data = setColor(this.computedColor, data)
}
return this.$createElement('div', data, [this.genWrapper()])
},
/** @public */
toggle () {
this.isActive = !this.isActive
},
},
render (h): VNode {
const render = this.genAlert()
if (!this.transition) return render
return h('transition', {
props: {
name: this.transition,
origin: this.origin,
mode: this.mode,
},
}, [render])
},
})
vs class
classを使うとDecoratorを使う
その際の問題を1つサンプルに
Vuexの標準的なapiであるmapStateでmapしたものをclassの中等で参照すると型エラーがでる
Decoratorはあくまでも暗黙的に拡張してくれるだけで、標準的な型システムでは拡張されていることを認識できない
@Component({
computed: {
mapState(['hello', 'world'])
},
})
export default class HelloWorld extends Vue {
mounted() {
this.hello // ERROR!!!!!!
}
}
導入タイミング
現在まだrfc段階だが、この段階まで進めばもうほぼ仕様は変わらないらしい
多少多少関数名や使用感等が変わってもdryそんなになければ追従コストもそこまでではないんじゃないかと
なので、Vue Tsで型に困っているプロダクトにはもう組み込んでもいいかなと思います
留意点
composition apiはあくまでもVue.jsのリアクティブシステムの上に乗かっているapi。reactのhooksと同じ使用感ではない。かなりシンプルなapi。しかし、react hooksでやれることは大概やれるので便利なhooksがあったら移植するとか、知見の流用がしやすくなる部分もある。
また、Vue.jsはプログレッシブなフレームワークなので、composition apiが出たからと言って必ず使わないといけないわけではない。あくまでも型を守ったりロジック再利用の開発したい際の有力な選択肢が1つ増えるだけである。
Vue.jsらしく
チームやプロジェクトの段階に合う開発をすれば良い
ありがとうございました
Composition API
By kahirokunn
Composition API
- 2,136