Vue Composition API
my new best friend
What advantages does Vue* have compared to React*?
* I'm talking about the Vue Options/Class APIs compared to React's Options/Class APIs
What disadvantages does Vue have compared to React?
Vue Composition API fixes all this.
A simple example
<template>
<button @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
}
</script>
Look Ma!
No 'this'!
Why'd they change?
Mixins
Why'd they change?
Less Confusing Nomenclature
Why'd they change?
Seperation of Concerns
And now: an INCREDIBLY COMPLEX example
This is actually not using Vue 3.0, but Vue 2.0. While I recommend that you do give 3.0 a try, it might be a bit much to convert legacy 2.0 projects.
That shouldn't stop you however, as you can just add the Vue Composition API as a plugin - like Vuex or Vue Router - and use it in Vue 2.0 codebases. (That's right: Try before you buy!)
# BASH
$ yarn add @vue/composition-api
/* ------ */
/* in main.ts/main.js */
import VueComposition from '@vue/composition-api';
Vue.use(VueComposition)
old component
new component
import { Component, Prop, Watch, Vue } from 'vue-property-decorator'
import { Action } from 'vuex-class'
import { IRoom, IEvent } from '../types'
@Component
export default class RoomClock extends Vue {
@Prop({ required: true }) roomEvents: Array<IEvent>
@Action('getEventsByRoom') getEventsByRoom: Function
ctx: CanvasRenderingContext2D | null
height: number = 0
width: number = 0
interval: number = 0
private CLOCK = {
PADDING: 0,
BORDER: 14,
DEFAULT: 'rgba(255,255,255, 0.1)',
HOUR_HAND: 'white',
BUSY: ['#FB4352', '#22CDAD', '#FEE44D', '#1EA1FC'],
RADIUS_DIVIDER: 2.2
}
$refs: {
container: Element
clockCanvas: HTMLCanvasElement
}
get x() {
return this.width / 2
}
get y() {
return this.height / 2
}
get radius() {
return (this.x * 94) / 100
}
randomColor() {
return this.CLOCK.BUSY[Math.floor(Math.random() * this.CLOCK.BUSY.length)]
}
@Watch('roomEvents')
draw() {
this.setCanvas()
this.drawOuterCircle()
this.drawOccupiedHours()
this.drawCurrentHourIndicator()
}
setCanvas() {
// check is container & canvas is avaliable
if (!this.$refs.container) return null
this.ctx = this.$refs.clockCanvas.getContext('2d')
// once both of them are avaliable
if (this.$refs.container.parentElement) {
const { width, height } = this.$refs.container.parentElement.getBoundingClientRect()
this.width = width + this.CLOCK.PADDING
this.height = height + this.CLOCK.PADDING
}
if (this.ctx) {
// clear everything
this.ctx.clearRect(0, 0, this.width, this.height)
return true
}
}
drawOuterCircle() {
if (this.ctx) {
this.ctx.beginPath()
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false)
this.ctx.lineWidth = this.CLOCK.BORDER
this.ctx.strokeStyle = this.CLOCK.DEFAULT
this.ctx.stroke()
}
}
async drawOccupiedHours() {
if (this.ctx) {
// get the data
this.ctx.lineWidth = this.CLOCK.BORDER
// draw for all current and future occupied events
this.roomEvents.forEach(item => {
if (new Date() < new Date(item.End) && this.ctx) {
const startAngle = this.getAngle(item.Start)
const endAngle = this.getAngle(item.End)
// draw arc
this.ctx.beginPath()
this.ctx.strokeStyle = this.randomColor()
this.ctx.arc(this.x, this.y, this.radius, startAngle, endAngle)
this.ctx.stroke()
}
})
return true
}
}
drawCurrentHourIndicator() {
const now = new Date()
const nowPlusMinute = new Date()
nowPlusMinute.setMinutes(nowPlusMinute.getMinutes() + 2)
const startAngle = this.getAngle(now.toJSON())
const endAngle = this.getAngle(nowPlusMinute.toJSON())
if (this.ctx) {
// draw arc
this.ctx.beginPath()
this.ctx.strokeStyle = this.CLOCK.HOUR_HAND
this.ctx.lineWidth = this.CLOCK.BORDER
this.ctx.arc(this.x, this.y, this.radius, startAngle, endAngle)
this.ctx.stroke()
}
}
getAngle(timeStr: any) {
const time = new Date(timeStr)
const h = time.getHours()
const m = time.getMinutes()
const s = time.getSeconds()
// convert hours, minutes and seconds to angle
const angle = ((h % 12) * Math.PI) / 6 + (m * Math.PI) / (6 * 60) + (s * Math.PI) / (360 * 60)
// remove 90 deg from the angle to start at 12 O'clock
return angle - 0.5 * Math.PI
}
}
import {
defineComponent,
reactive,
ref,
watch,
toRefs,
onMounted,
} from '@vue/composition-api'
import { IEvent } from '../../types'
import { RoomClockProps, RoomClockState } from './types'
import useCanvas from './hooks/useCanvas'
const RoomClock = defineComponent({
name: 'room-clock',
setup(props: RoomClockProps, vm: unknown) {
const container = ref<null | HTMLElement>(null)
const clockCanvas = ref<null | HTMLCanvasElement>(null)
const roomEvents: IEvent[] = props.roomEvents
const state = reactive<RoomClockState>({
ctx: null,
height: 0,
width: 0,
interval: 0
})
const draw = useCanvas({ container, clockCanvas, state, roomEvents })
onMounted(() => {
draw()
})
watch(
() => roomEvents,
() => draw()
)
const { width, height } = toRefs(state)
return { width, height, container, clockCanvas }
}
})
old component
import { Component, Prop, Watch, Vue } from 'vue-property-decorator'
import { Action } from 'vuex-class'
import { IRoom, IEvent } from '../types'
@Component
export default class RoomClock extends Vue {
@Prop({ required: true }) roomEvents: Array<IEvent>
@Action('getEventsByRoom') getEventsByRoom: Function
ctx: CanvasRenderingContext2D | null
height: number = 0
width: number = 0
interval: number = 0
private CLOCK = {
PADDING: 0,
BORDER: 14,
DEFAULT: 'rgba(255,255,255, 0.1)',
HOUR_HAND: 'white',
BUSY: ['#FB4352', '#22CDAD', '#FEE44D', '#1EA1FC'],
RADIUS_DIVIDER: 2.2
}
$refs: {
container: Element
clockCanvas: HTMLCanvasElement
}
get x() {
return this.width / 2
}
get y() {
return this.height / 2
}
get radius() {
return (this.x * 94) / 100
}
randomColor() {
return this.CLOCK.BUSY[Math.floor(Math.random() * this.CLOCK.BUSY.length)]
}
@Watch('roomEvents')
draw() {
this.setCanvas()
this.drawOuterCircle()
this.drawOccupiedHours()
this.drawCurrentHourIndicator()
}
setCanvas() {
// check is container & canvas is avaliable
if (!this.$refs.container) return null
this.ctx = this.$refs.clockCanvas.getContext('2d')
// once both of them are avaliable
if (this.$refs.container.parentElement) {
const { width, height } = this.$refs.container.parentElement.getBoundingClientRect()
this.width = width + this.CLOCK.PADDING
this.height = height + this.CLOCK.PADDING
}
if (this.ctx) {
// clear everything
this.ctx.clearRect(0, 0, this.width, this.height)
return true
}
}
drawOuterCircle() {
if (this.ctx) {
this.ctx.beginPath()
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false)
this.ctx.lineWidth = this.CLOCK.BORDER
this.ctx.strokeStyle = this.CLOCK.DEFAULT
this.ctx.stroke()
}
}
async drawOccupiedHours() {
if (this.ctx) {
// get the data
this.ctx.lineWidth = this.CLOCK.BORDER
// draw for all current and future occupied events
this.roomEvents.forEach(item => {
if (new Date() < new Date(item.End) && this.ctx) {
const startAngle = this.getAngle(item.Start)
const endAngle = this.getAngle(item.End)
// draw arc
this.ctx.beginPath()
this.ctx.strokeStyle = this.randomColor()
this.ctx.arc(this.x, this.y, this.radius, startAngle, endAngle)
this.ctx.stroke()
}
})
return true
}
}
drawCurrentHourIndicator() {
const now = new Date()
const nowPlusMinute = new Date()
nowPlusMinute.setMinutes(nowPlusMinute.getMinutes() + 2)
const startAngle = this.getAngle(now.toJSON())
const endAngle = this.getAngle(nowPlusMinute.toJSON())
if (this.ctx) {
// draw arc
this.ctx.beginPath()
this.ctx.strokeStyle = this.CLOCK.HOUR_HAND
this.ctx.lineWidth = this.CLOCK.BORDER
this.ctx.arc(this.x, this.y, this.radius, startAngle, endAngle)
this.ctx.stroke()
}
}
getAngle(timeStr: any) {
const time = new Date(timeStr)
const h = time.getHours()
const m = time.getMinutes()
const s = time.getSeconds()
// convert hours, minutes and seconds to angle
const angle = ((h % 12) * Math.PI) / 6 + (m * Math.PI) / (6 * 60) + (s * Math.PI) / (360 * 60)
// remove 90 deg from the angle to start at 12 O'clock
return angle - 0.5 * Math.PI
}
}
All of these concern static data and derived
data for the clock, so they were changed from observables to plain functions and seperated into ./utils/clock.ts
All of these concern drawing on the canvas, so they were moved to useCanvas()
...and changed from managed observable to plain function
which in fact, didn't need to be imported into the main component at all.
Hey Present Brian. This is Past Brian telling you to switch over to the code now.
Key takeaways
- We seperate concerns, such as seperating the "Clock" logic from the "Canvas" logic
- It's extremely clear what's a "prop", what's "state", what's a method, and what's a derived value, instead of having it all on 'this'
- In fact, we never have to keep track of what context 'this' is, because we don't use 'this'
- Everything is properly encapsulated - the template doesn't have access to any properties or methods that it doesn't need.
- Everything is just a function - so you can test it independently like any other function, without worrying about all the interdependencies on it.
- A lot of stuff that was observable, turns out, doesn't have to be, so we can simplify the logic and thereby simplify future maintainance.
Questions?
Vue Composition API: my new best friend
By brianboyko
Vue Composition API: my new best friend
- 871