Skip to content

Latest commit

 

History

History
1165 lines (775 loc) · 19.6 KB

example2.md

File metadata and controls

1165 lines (775 loc) · 19.6 KB
layout download highlighter info
cover
shiki
## Composable Vue Pattens and tips for writing good composable logic in Vue [Anthony Fu](https://antfu.me/) at [VueDay 2021](https://2021.vueday.it/) - [Recording](https://www.youtube.com/watch?v=IMJjP6edHd0) - [Transcript](https://antfu.me/posts/composable-vue-vueday-2021) - [Source code](https://github.com/antfu/talks/tree/master/2021-04-29)

Composable Vue

Pattens and tips for writing good composable logic in Vue

Anthony Fu
VueDay
Apr. 29th, 2021

layout: 'intro'

Anthony Fu

Vue core team member and Vite team member.
Creator of VueUse, i18n Ally and Type Challenges.
A fanatical full-time open sourceror.


name: Sponsors layout: center



layout: center

Composable Vue


name: VueUse layout: center

Collection of essential Vue Composition Utilities
NPM version NPM Downloads Docs & Demos Function Count
GitHub stars
  • Works for both Vue 2 and 3
  • Tree-shakeable ESM
  • CDN compatible
  • TypeScript
  • Rich ecosystems

layout: center class: text-center

Composition API

a brief go-through


Ref

import { ref } from 'vue'

let foo = 0
let bar = ref(0)

foo = 1
bar = 1 // ts-error
Pros
  • More explicit, with type checking
  • Less caveats
Cons
  • .value

Reactive

import { reactive } from 'vue'

const foo = { prop: 0 }
const bar = reactive({ prop: 0 })

foo.prop = 1
bar.prop = 1
Pros
  • Auto unwrapping (a.k.a .value free)
Cons
  • Same as plain objects on types
  • Destructure loses reactivity
  • Need to use callback for watch

Ref Auto Unwrapping

Get rid of .value for most of the time.

  • watch accepts ref as the watch target, and returns the unwrapped value in the callback
const counter = ref(0)

watch(counter, count => {
  console.log(count) // same as `counter.value`
})
  • Ref is auto unwrapped in the template
<template>
  <button @click="counter += 1">
    Counter is {{ counter }}
  </button>
</template>
  • Reactive will auto-unwrap nested refs.
import { ref, reactive } from 'vue'
const foo = ref('bar')
const data = reactive({ foo, id: 10 })
data.foo // 'bar'

unref - Oppsite of Ref

  • If it gets a Ref, returns the value of it.
  • Otherwise, returns as-is.
Implementation
function unref<T>(r: Ref<T> | T): T {
  return isRef(r) ? r.value : r
}
Usage
import { unref, ref } from 'vue'

const foo = ref('foo')
unref(foo) // 'foo'

const bar = 'bar'
unref(bar) // 'bar'

layout: center class: text-center

Patterns & Tips

of writing composable functions


What's Composable Functions

Sets of reusable logic, separation of concerns.

export function useDark(options: UseDarkOptions = {}) {
  const preferredDark = usePreferredDark()         // <--
  const store = useStorage('vueuse-dark', 'auto')  // <--

  return computed<boolean>({
    get() {
      return store.value === 'auto'
        ? preferredDark.value
        : store.value === 'dark'
    },
    set(v) {
      store.value = v === preferredDark.value 
        ? 'auto' : v ? 'dark' : 'light'
    },
  })
}

Think as "Connections"

The setup() only runs once on component initialization, to construct the relations between your state and logic.

  • Input → OutputEffects
  • Output reflects to input's changes automatically

SpreadSheet Formula


One Thing at a Time

Just the same as authoring JavaScript functions.

  • Extract duplicated logics into composable functions
  • Have meaningful names
  • Consistent naming conversions - useXX createXX onXX
  • Keep function small and simple
  • "Do one thing, and do it well"

Passing Refs as Arguments

Implementation
Usage
Plain function
function add(a: number, b: number) {
  return a + b
}
let a = 1
let b = 2

let c = add(a, b) // 3
Accpets refs,
returns a reactive result.
function add(a: Ref<number>, b: Ref<number>) {
  return computed(() => a.value + b.value)
}
const a = ref(1)
const b = ref(2)

const c = add(a, b)
c.value // 3
Accpets both refs and plain values.
function add(
  a: Ref<number> | number,
  b: Ref<number> | number
) {
  return computed(() => unref(a) + unref(b))
}
const a = ref(1)

const c = add(a, 5)
c.value // 6

MaybeRef

A custom type helper

type MaybeRef<T> = Ref<T> | T

In VueUse, we use this helper heavily to support optional reactive arguments

export function useTimeAgo(
  time: Date | number | string | Ref<Date | number | string>,
) {
  return computed(() => someFormating(unref(time)))
}
import { computed, unref, Ref } from 'vue'

type MaybeRef<T> = Ref<T> | T

export function useTimeAgo(
  time: MaybeRef<Date | number | string>,
) {
  return computed(() => someFormating(unref(time)))
}

Make it Flexible

Make your functions like LEGO, can be used with different components in different ways.

Create a "Special" Ref
import { useTitle } from '@vueuse/core'

const title = useTitle()

title.value = 'Hello World'
// now the page's title changed
Binding an Existing Ref
import { ref, computed } from 'vue'
import { useTitle } from '@vueuse/core'

const name = ref('Hello')
const title = computed(() => {
  return `${name.value} - World`
})

useTitle(title) // Hello - World

name.value = 'Hi' // Hi - World

useTitle Case

Take a look at useTitle's implementation

import { ref, watch } from 'vue'
import { MaybeRef } from '@vueuse/core'

export function useTitle(
  newTitle: MaybeRef<string | null | undefined>
) {
  const title = ref(newTitle || document.title)

  watch(title, (t) => {
    if (t != null)
      document.title = t
  }, { immediate: true })

  return title
}

<-- 1. use the user provided ref or create a new one

<-- 2. sync ref changes to the document title

"Reuse" Ref

If you pass a ref into ref(), it will return the original ref as-is.

const foo = ref(1)   // Ref<1>
const bar = ref(foo) // Ref<1>

foo === bar // true
function useFoo(foo: Ref<string> | string) {
  // no need!
  const bar = isRef(foo) ? foo : ref(foo)

  // they are the same
  const bar = ref(foo)

  /* ... */
}

Extremely useful in composable functions that take uncertain argument types.


ref / unref

  • MaybeRef<T> works well with ref and unref.
  • Use ref() when you want to normalized it as a Ref.
  • Use unref() when you want to have the value.

type MaybeRef<T> = Ref<T> | T

function useBala<T>(arg: MaybeRef<T>) {
  const reference = ref(arg) // get the ref
  const value = unref(arg)   // get the value
}

Object of Refs

Getting benefits from both ref and reactive for authoring composable functions

import { ref, reactive } from 'vue'

function useMouse() {
  return { 
    x: ref(0),
    y: ref(0)
  }
}

const { x, y } = useMouse()
const mouse = reactive(useMouse())

mouse.x === x.value // true
  • Destructurable as Ref
  • Convert to reactive object to get the auto-unwrapping when needed

Async to "Sync"

With Composition API, we can actually turn async data into "sync"

Async
const data = await fetch('https://api.github.com/').then(r => r.json())

// use data
Composition API
const { data } = useFetch('https://api.github.com/').json()

const user_url = computed(() => data.value?.user_url)

Establish the "Connections" first, then wait for data to be filled up. The idea is similar to SWR (stale-while-revalidate)


useFetch Case

export function useFetch<R>(url: MaybeRef<string>) {
  const data = shallowRef<T | undefined>()
  const error = shallowRef<Error | undefined>()

  fetch(unref(url))
    .then(r => r.json())
    .then(r => data.value = r)
    .catch(e => error.value = e)

  return {
    data,
    error
  }
}

Side-effects Self Cleanup

The watch and computed will stop themselves on components unmounted.
We'd recommend following the same pattern for your custom composable functions.

import { onUnmounted } from 'vue'

export function useEventListener(target: EventTarget, name: string, fn: any) {
  target.addEventListener(name, fn)

  onUnmounted(() => {
    target.removeEventListener(name, fn) // <--
  })
}

effectScope RFC Upcoming

A new API to collect the side effects automatically. Likely to be shipped with Vue 3.1
vuejs/rfcs#212

// effect, computed, watch, watchEffect created inside the scope will be collected

const scope = effectScope(() => {
  const doubled = computed(() => counter.value * 2)

  watch(doubled, () => console.log(double.value))

  watchEffect(() => console.log('Count: ', double.value))
})

// dispose all effects in the scope
stop(scope)

disabled: true

Template Ref

To get DOM element, you can pass a ref to it, and it will be available after component mounted

import { defineComponent, ref, onMounted } from 'vue'
export default defineComponent({
  setup() {
    const element = ref<HTMLElement | undefined>()

    onMounted(() => {
      element.value // now you have it
    })

    return { element }
  }
})
<template>
  <div ref="element"><!-- ... --></div>
</template>

disabled: true

Template Ref

Use watch instead of onMounted to unify the handling for template ref changes.

import { defineComponent, ref, watch } from 'vue'
export default defineComponent({
  setup() {
    const element = ref<HTMLElement | undefined>()

    watch(element, (el) => {
      // clean up previous side effect
      if (el) {
        // use the DOM element
      }
    })

    return { element }
  }
})

Typed Provide / Inject

Use the InjectionKey<T> helper from Vue to share types across context.

// context.ts
import { InjectionKey } from 'vue'

export interface UserInfo {
  id: number
  name: string
}

export const injectKeyUser: InjectionKey<UserInfo> = Symbol()

Typed Provide / Inject

Import the key from the same module for provide and inject.

// parent.vue
import { provide } from 'vue' 
import { injectKeyUser } from './context'

export default {
  setup() {
    provide(injectKeyUser, {
      id: '7', // type error: should be number
      name: 'Anthony'
    })
  }
}
// child.vue
import { inject } from 'vue' 
import { injectKeyUser } from './context'

export default {
  setup() {
    const user = inject(injectKeyUser) 
    // UserInfo | undefined

    if (user)
      console.log(user.name) // Anthony
  }
}

Shared State

By the nature of Composition API, states can be created and used independently.

// shared.ts
import { reactive } from 'vue'

export const state = reactive({
  foo: 1,
  bar: 'Hello'
})
// A.vue
import { state } from './shared.ts'

state.foo += 1
// B.vue
import { state } from './shared.ts'

console.log(state.foo) // 2

⚠️ But it's not SSR compatible!


Shared State (SSR friendly)

Use provide and inject to share the app-level state

export const myStateKey: InjectionKey<MyState> = Symbol()

export function createMyState() {
  const state = {
    /* ... */
  }

  return {
    install(app: App) {
      app.provide(myStateKey, state)
    }
  }
}

export function useMyState(): MyState {
  return inject(myStateKey)!
}
// main.ts
const App = createApp(App)

app.use(createMyState())
// A.vue

// use everywhere in your app
const state = useMyState()

useVModel

A helper to make props/emit easier

export function useVModel(props, name) {
  const emit = getCurrentInstance().emit

  return computed({
    get() {
      return props[name]
    },
    set(v) {
      emit(`update:${name}`, v)
    }
  })
}
export default defineComponent({
  setup(props) {
    const value = useVModel(props, 'value')

    return { value }
  }
})

<template>
  <input v-model="value" />
</template>

disabled: true

useVModel (Passive)

Make the model able to be updated independently from the parent logic

export function usePassiveVModel(props, name) {
  const emit = getCurrentInstance().emit
  const data = ref(props[name])                     // store the value in a ref

  watch(() => props.value, (v) => data.value = v)   // sync the ref whenever the prop changes

  return computed({
    get() {
      return data.value
    },
    set(v) {
      data.value = v                                 // when setting value, update the ref directly
      emit(`update:${name}`, v)                      // then emit out the changes
    }
  })
}

layout: center

All of them work for both Vue 2 and 3


@vue/composition-api Lib

Composition API support for Vue 2.
vuejs/composition-api

import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'

Vue.use(VueCompositionAPI)
import { ref, reactive } from '@vue/composition-api'

Vue 2.7 Upcoming

Plans in Vue 2.7

  • Backport @vue/composition-api into Vue 2's core.
  • <script setup> syntax in Single-File Components.
  • Migrate codebase to TypeScript.
  • IE11 support.
  • LTS.

Vue Demi Lib

Creates Universal Library for Vue 2 & 3
vueuse/vue-demi

// same syntax for both Vue 2 and 3
import { ref, reactive, defineComponent } from 'vue-demi'

Recap

  • Think as "Connections"
  • One thing at a time
  • Accepting ref as arguments
  • Returns an object of refs
  • Make functions flexible
  • Async to "sync"
  • Side-effect self clean up
  • Shared state

layout: center class: 'text-center pb-5 :'

Thank You!

Slides can be found on antfu.me