Slide 1

Slide 1 text

THE COMPOSITION API

Slide 2

Slide 2 text

THE COMPOSITION API EMERGING PATTERNS & BEST PRACTICES

Slide 3

Slide 3 text

HI, I’M THORSTEN

Slide 4

Slide 4 text

HI, I’M THORSTEN

Slide 5

Slide 5 text

PREREQUISITES I’VE SEEN GREGG’S TALK I’VE PLAYED WITH THE COMPOSITION API

Slide 6

Slide 6 text

EMERGING PATTERNS &BEST PRACTICES

Slide 7

Slide 7 text

Vuelidate@next vue-apollo@next awesomejs.dev vuex-feathers villus (tiny GraphQL client)

Slide 8

Slide 8 text

SO I STARTED READING A LOT OF CODE … LIKE… A LOT! … AND COLLECTED IMPRESSIONS ON POST-IT NOTES

Slide 9

Slide 9 text

REF() VS. REACTIVE() … WHICH SHOULD I USE? 50% of comments in the RFC

Slide 10

Slide 10 text

https://timqian.com/chart.xkcd A lot of ref() Some lonely 
 reactive()

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

DISCLAIMER: MOST OF THIS CODE IS LIBRARY CODE Practices for use in components may come out different

Slide 13

Slide 13 text

CONSISTENCY

Slide 14

Slide 14 text

SOMETIMES, REACTIVE() DOESN’T WORK* and you rather want/need a ref * at least not in an ergonomic way

Slide 15

Slide 15 text

DEVELOPERS VALUE CONSISTENCY

Slide 16

Slide 16 text

COMPUTED PROPERTIES ARE REFS

Slide 17

Slide 17 text

export function exampleWithComputed() { const state = reactive({ a: 1, b: 2, x: 2, }) const sum = computed(() => state.a + state.b) const squared = computed(() => sum.value ** state.x) return toRefs({ ...state, sum, squared, }) }

Slide 18

Slide 18 text

export function exampleWithRefs() { const a = ref(1) const b = ref(2) const x = ref(2) const sum = computed(() => a.value + b.value) const squared = computed(() => sum.value ** x.value) return toRefs({ a, b, x, sum, squared, }) } CONSISTENCY

Slide 19

Slide 19 text

DOM REFERENCES REQUIRE (TEMPLATE) REFS

Slide 20

Slide 20 text

setup() { const inputEl = ref(null) onMounted(() => { inputEl.value.addEventListener(/* */) }) return { inputEl, } }

Slide 21

Slide 21 text

IF DEVELOPERS VALUE CONSISTENCY they likely prefer refs, as those work everywhere

Slide 22

Slide 22 text

SO… ARE YOU TELLING ME THAT REACTIVE() IS USELESS? most of your, probably

Slide 23

Slide 23 text

OF COURSE NOT! If you want to use it, do!

Slide 24

Slide 24 text

IT’S A QUESTION OF PERSONAL PREFERENCE Just accept that you can’t completely evade refs

Slide 25

Slide 25 text

EMERGING BEST PRACTICES by example

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

setup (props, { root }) { // Form data const projectTypeId = ref(root.$route.query.projectTypeId || null) const formData = reactive({ packageName: root.$route.query.packageName || '', tags: [], }) watch(() => root.$route, value => { projectTypeId.value = value.query.projectTypeId || null formData.packageName = value.query.packageName || '' }) // Check for existing proposals & packages const { result, loading } = useQuery(gql` query PackageProposalAndPackageByName ($name: String!) { proposal: packageProposalByName (name: $name) { ...pkgProposal projectTypes { id name slug } } pkg: packageByName (name: $name) { ...pkg projectTypes { id name slug } } } ${pkgFragment} ${pkgProposalFragment} `, () => ({ name: formData.packageName, }), () => ({ enabled: !!formData.packageName, debounce: 1000, })) const proposal = useResult(result, null, data => data.proposal) const pkg = useResult(result, null, data => data.pkg) const alreadyProposed = computed(() => formData.packageName && !loading.value && proposal.value) const alreadyExists = computed(() => formData.packageName && !loading.value && pkg.value) // Form validation const requiredFieldsValid = computed(() => projectTypeId.value != null && !!formData.packageName) const valid = computed(() => requiredFieldsValid.value && !alreadyProposed.value && !alreadyExists.value) // Added summary const added = ref(false) const addedProposal = ref(null) // Submit

Slide 29

Slide 29 text

function usePackageCheck(formData){} function useFormValidation(projectTypeId, formData) {} function useSubmit(valid, formData, projectTpeId, rerquiredFieldsValid) {} setup(props, { root }) { // Initial state setup left out // Check for existing proposals & packages const { proposal, pkg, alreadyExists, alreadyProposed, } = usePackageCheck(formData); // Form validation const valid = useFormValidation(projectTypeId, formData); // Submit const { error, submitting, submit } = useSubmit( valid, formData, projectTypeId, requiredFieldsValid ); // NPM search const { searchText: npmSearchText, result: npmSearchResult } = useNpmSearch( { hitsPerPage: 5

Slide 30

Slide 30 text

function usePackageCheck(formData){} function useFormValidation(projectTypeId, formData) {} function useSubmit(valid, formData, projectTpeId, rerquiredFieldsValid) {} setup(props, { root }) { // Initial state setup left out // Check for existing proposals & packages const { proposal, pkg, alreadyExists, alreadyProposed, } = usePackageCheck(formData); // Form validation const valid = useFormValidation(projectTypeId, formData); // Submit const { error, submitting, submit } = useSubmit( valid, formData, projectTypeId, requiredFieldsValid ); // …

Slide 31

Slide 31 text

function usePackageCheck(formData){} function useFormValidation(projectTypeId, formData) {} function useSubmit(valid, formData, projectTpeId, rerquiredFieldsValid) {} setup(props, { root }) { // Initial state setup left out // Check for existing proposals & packages const { proposal, pkg, alreadyExists, alreadyProposed, } = usePackageCheck(formData); // Form validation const valid = useFormValidation(projectTypeId, formData); // Submit const { error, submitting, submit } = useSubmit( valid, formData, projectTypeId, requiredFieldsValid ); // …

Slide 32

Slide 32 text

function usePackageCheck(formData){} function useFormValidation(projectTypeId, formData) {} function useSubmit(valid, formData, projectTpeId, rerquiredFieldsValid) {} setup(props, { root }) { // Initial state setup left out // Check for existing proposals & packages const { proposal, pkg, alreadyExists, alreadyProposed, } = usePackageCheck(formData); // Form validation const valid = useFormValidation(projectTypeId, formData); // Submit const { error, submitting, submit } = useSubmit( valid, formData, projectTypeId, requiredFieldsValid ); // …

Slide 33

Slide 33 text

export function myCompositionFunction(arg1, arg2) { /* The magic happens here */ return { refs, objects, functions, } } Dealing with arguments Implementation Tripwires Returning the right way

Slide 34

Slide 34 text

HANDLING REFS IN ARGUMENTS

Slide 35

Slide 35 text

export function useWithRef(someFlag: Ref) { watch(ref, val => { /* do something */ }) return {} } const result2 = useWithRef(true) TYPE ERROR! const isActive = ref(true) const result = useWithRef(isActive)

Slide 36

Slide 36 text

export function useWithRef(someFlag: Ref) { if (!isRef(someFlag)) warn('Needs a ref') watch(ref, val => { /* do something */ }) return {} } const isActive = ref(true) const result = useWithRef(isActive) const result = useWithRef(ref(true))

Slide 37

Slide 37 text

CAN WE ACCEPT BOTH REF & STATIC VALUES?

Slide 38

Slide 38 text

export function useEvent( el: Ref | Element, name: string, listener: EventListener, options?: boolean | AddEventListenerOptions ) { const element = wrap(el as Element) onMounted(() => element.value!.addEventListener(name, listener, options)) onUnmounted(() => element.value!.removeEventListener(name, listener)) }

Slide 39

Slide 39 text

export function useEvent( el: Ref | Element, name: string, listener: EventListener, options?: boolean | AddEventListenerOptions ) { const element = wrap(el as Element) onMounted(() => element.value!.addEventListener(name, listener, options)) onUnmounted(() => element.value!.removeEventListener(name, listener)) } const wrap = (value) => (isRef(value) ? value : ref(value))

Slide 40

Slide 40 text

FORGIVING API VS. STRICT API

Slide 41

Slide 41 text

LIFECYCLE HOOKS VS. WATCH

Slide 42

Slide 42 text

‣ What if the ref is empty on mount? ‣ What if the ref changes later? export function useEvent(_el, name, listener, options) { const element = wrap(_el) onMounted(() => element.value.addEventListener(name, listener, options)) onUnmounted(() => element.value.removeEventListener(name, listener)) }

Slide 43

Slide 43 text

WATCH() TO THE RESCUE

Slide 44

Slide 44 text

export function useEvent(_el, name, listener, options) { const element = wrap(_el) onMounted(() => element.value.addEventListener(name, listener, options)) onUnmounted(() => element.value.removeEventListener(name, listener)) }

Slide 45

Slide 45 text

export function useEvent(_el, name, listener, options) { const element = wrap(_el) watch(element, (el, _, onCleanup) => { el && el.addEventListener(name, listener, options) }) onMounted(() => element.value.addEventListener(name, listener, options)) onUnmounted(() => element.value.removeEventListener(name, listener)) }

Slide 46

Slide 46 text

export function useEvent(_el, name, listener, options) { const element = wrap(_el) watch(element, (el, _, onCleanup) => { el && el.addEventListener(name, listener, options) }) onUnmounted(() => element.value.removeEventListener(name, listener)) }

Slide 47

Slide 47 text

export function useEvent(_el, name, listener, options) { const element = wrap(_el) watch(element, (el, _, onCleanup) => { el && el.addEventListener(name, listener, options) onCleanup(() => el && el.removeEventListener(name, listener)) }) onUnmounted(() => element.value.removeEventListener(name, listener)) }

Slide 48

Slide 48 text

export function useEvent(_el, name, listener, options) { const element = wrap(_el) watch(element, (el, _, onCleanup) => { el && el.addEventListener(name, listener, options) onCleanup(() => el && el.removeEventListener(name, listener)) }) } ‣ Listeners are only added when the element actually exists ‣ listeners are updated when element ref changes

Slide 49

Slide 49 text

RETURN COMPUTED > RETURN REF

Slide 50

Slide 50 text

createComponent({ setup() { const isOnline = useOnline() return { isOnline, } }, })

Slide 51

Slide 51 text

import { ref, computed, onUnmounted } from '@vue/composition-api' export default function useOnline() { const isOnline = ref(true) isOnline.value = window.navigator ? window.navigator.onLine : true const onlineHandler = () => (isOnline.value = true) const offlineHandler = () => (isOnline.value = false) window.addEventListener('online', onlineHandler, false) window.addEventListener('offline', offlineHandler, false) onUnmounted(() => { window.removeEventListener('online', onlineHandler) window.removeEventListener('offline', offlineHandler) }) return isOnline } This ref is mutable!

Slide 52

Slide 52 text

import { ref, computed, onUnmounted } from '@vue/composition-api' export default function useOnline() { const isOnline = ref(true) isOnline.value = window.navigator ? window.navigator.onLine : true const onlineHandler = () => (isOnline.value = true) const offlineHandler = () => (isOnline.value = false) window.addEventListener('online', onlineHandler, false) window.addEventListener('offline', offlineHandler, false) onUnmounted(() => { window.removeEventListener('online', onlineHandler) window.removeEventListener('offline', offlineHandler) }) return computed(() => isOnline.value) }

Slide 53

Slide 53 text

import { ref, computed, onUnmounted } from '@vue/composition-api' export default function useOnline() { const isOnline = ref(true) isOnline.value = window.navigator ? window.navigator.onLine : true const onlineHandler = () => (isOnline.value = true) const offlineHandler = () => (isOnline.value = false) window.addEventListener('online', onlineHandler, false) window.addEventListener('offline', offlineHandler, false) onUnmounted(() => { window.removeEventListener('online', onlineHandler) window.removeEventListener('offline', offlineHandler) }) return readonly({ isOnline, a: 'A', b: 'B', }) }

Slide 54

Slide 54 text

NAME RETURNED PROPERTIES IN CONTEXT

Slide 55

Slide 55 text

export function useFullscreen(target: Ref) { const isFullscreen = ref(false) function exitFullscreen() { if (document.fullscreenElement) { document.exitFullscreen() } isFullscreen.value = false } async function enterFullscreen() { exitFullscreen() if (!target.value) return await target.value.requestFullscreen() isFullscreen.value = true } return { isFullscreen, enterFullscreen, exitFullscreen, } }

Slide 56

Slide 56 text

createComponent({ setup() { const el = ref(null) const fullscreen = useFullscreen(el) onMounted(() => fullscreen.enterFullscreen) return { el, fullscreen, } }, })

Slide 57

Slide 57 text

export function useFullscreen(target: Ref) { const isActive = ref(false) function exit() { if (document.fullscreenElement) { document.exitFullscreen() } isActive.value = false } async function enter() { exit() if (!target.value) return await target.value.requestFullscreen() isActive.value = true } return { isActive, enter, exit, } }

Slide 58

Slide 58 text

createComponent({ setup() { const el = ref(null) const fullscreen = useFullscreen(el) onMounted(fullscreen.enter) return { el, fullscreen, } }, })

Slide 59

Slide 59 text

createComponent({ setup() { const el = ref(null) const { enter: enterFullscreen } = useFullscreen(el) onMounted(enterFullscreen) return { el, enterFullScreen, } }, })

Slide 60

Slide 60 text

THANKS! Twitter: @linus_borg Github: linusborg