Nuxt.js and the

Composition API

Developer Group Leipzig - 01. Juni 2021

About me

Alexander Lichter

Nuxt.js Core Maintainer

@TheAlexLichter

Web Dev Consultant

@TheAlexLichter

Status Quo

Vue 3 + CAPI out for half a year now

but the ecosystem is still catching up

Vue 2 + CAPI is a viable option

More and more libraries provide composables, e.g. vue-apollo or vue-i18n

Plans to move CAPI inside the Vue 2 core

Goal of the Vue 2.x

Composition API Plugin

@TheAlexLichter

[...] primary goal is to provide a way to experiment with the API and to collect feedback.

 

When you migrate to Vue 3, just replacing @vue/composition-api to vue and your code should just work.

Composition API in Nuxt

@TheAlexLichter

  • Already works fine with the CAPI plugin
  • Can be enabled via Nuxt plugin

Problems

  • Does not cover Nuxt functionalities
  • What is with SSR?

Nuxt.js

Composition API

@TheAlexLichter

  • Superset of @vue/composition-api
  • Composables for nuxt-specific functionalities, e.g.
    • Data fetching
    • Accessing the context
    • Receiving the current route
    • Setting meta tags
  • Under nuxt-community umbrella

Goals

@TheAlexLichter

Similar to the @vue/composition-api package

 

  1. Provide a way to use CAPI with Nuxt + Vue 2
  2. Get feedback for future composables
  3. Make future migration to Nuxt 3 easier

Notice

@TheAlexLichter

The API of nuxt-specific composables will

likely change before Nuxt 3 is released.

Talking about Nuxt 3...

@TheAlexLichter

Common composables

@TheAlexLichter

fetch in Nuxt.js

@TheAlexLichter

import axios from 'axios'

export default {
  data() {
    return {
      mountains: []
    }
  },
  async fetch() {
    this.mountains = await axios.get('https://api.nuxtjs.dev/mountains')
  }
}

useFetch

@TheAlexLichter

works like the fetch method of Nuxt.js

import { defineComponent, ref, useFetch } from '@nuxtjs/composition-api'
import axios from 'axios'

export default defineComponent({
  setup() {
    const mountains = ref([])

    useFetch(async () => {
      mountains.value = await axios.get('https://api.nuxtjs.dev/mountains')
    })

    return { mountains }
  }
})

useFetch

@TheAlexLichter

fetch and fetchState

import { defineComponent, ref, useFetch } from '@nuxtjs/composition-api'
import axios from 'axios'

export default defineComponent({
  setup() {
    const mountains = ref([])

    // Use fetch to re-fetch data
    // Use fetchState to check if error, pending or all good
    // Both exist in the template already ($fetch, $fetchState)
    const { fetch, fetchState } = useFetch(async () => {
      mountains.value = await axios.get('https://api.nuxtjs.dev/mountains')
    })

    return { mountains }
  }
})

The Context

@TheAlexLichter

Access the route, store and more!

export default {
  async asyncData(ctx) {
    const { slug } = ctx.params
    const mountain = await ctx.$axios.$get('https://api.nuxtjs.dev/mountains/' + slug)
    context.store.dispatch('initializeMountains')
    
    return { mountain }
  }
})

useRoute(r)

@TheAlexLichter

Similar to Vue3 router composables

import { computed, defineComponent, useRoute } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const route = useRoute()
    const id = computed(() => route.value.params.id)
  }
})
import { defineComponent, useRouter } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const router = useRouter()
    router.push('/')
  }
})

useStore

@TheAlexLichter

Similar to Vuex composable

import { defineComponent, useStore } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const store = useStore()
  }
})

useStore

@TheAlexLichter

...with types

import { defineComponent, useStore } from '@nuxtjs/composition-api'

export interface State {
  counter: number
}

export const key: InjectionKey<Store<State>> = Symbol()

export default defineComponent({
  setup() {
    const store = useStore(key)
    // or
    const store = useStore<State>()
    // Now, `store.state.counter` will be typed as a number
  }
})

useContext

@TheAlexLichter

All other belongings, e.g. modules!

import { defineComponent, useContext, slug } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    // There is the Nuxt.js context
    // You might know it from asyncData or middleware
    // It contains your module, the store and more
    // But better use useRoute(r), useStore and so on if possible
    const context = useContext()
    
    context.$http.$get('https://api.nuxtjs.dev/tips')
  }
})

useAsync

@TheAlexLichter

import { defineComponent, useAsync, useContext } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const { $http } = useContext()
    
    // Returns a null ref that is populated when the call resolves
    const posts = useAsync(() => $http.$get('https://api.nuxtjs.dev/mountains'))

    return { posts }
  }
})

Another way to fetch data

useAsync

@TheAlexLichter

const posts = useAsync(() => $http.$get('...'))

Node.js

Server

1. Request

4. HTML + JS

including useAsync response

API

2. useAsync call

3. Response

Initial request (SSR)

For the initial request, useAsync is only called on server-side

useAsync

@TheAlexLichter

const posts = useAsync(() => $http.$get('...'))

1. posts is null               useAsync called on navigation

API

2. posts has now a value (if the call succeeded)

Client-side navigation (CSR)

For further requests, useAsync is called on client-side. The navigation won't be blocked

useAsync

@TheAlexLichter

Not the exact equivalent of asyncData

Similarities

  • Both are blocking on the server-side
  • Both can be executed on server- and client-side
  • No way to "refetch" data nor built-in state check like fetch

Differences

  • useAsync is non-blocking on client-side while asyncData is blocking
  • Data from useAsync is not merged with data

useStatic

@TheAlexLichter

for expensive functions + SSG

export default defineComponent({
  setup() {
    const { params } = useRoute()
    const slug = computed(() => params.value.slug)
    const beer = useStatic(
      slug => axios.get(`https://api.nuxtjs.dev/beers/${slug.value}`), // 1
      slug, // 2
      'beers' // 3
    )

    return { beer }
  },
})
  1. factory function
  2. Parameter
  3. keyBase (unique across project)

useStatic

@TheAlexLichter

during generation

  • the factory function (1) will be executed
  • result will be saved as JSON named by the parameter (2) and keyBase (3)
    • when slug would be sternburg, the file would be saved under /static/beers-sternburg.json in your dist folder
const beer = useStatic(
  slug => axios.get(`https://api.nuxtjs.dev/beers/${slug}`), // 1
  slug, // 2
  'beers' // 3
)

useStatic

@TheAlexLichter

when your generated site is live

const beer = useStatic(
  slug => axios.get(`https://api.nuxtjs.dev/beers/${slug}`),
  slug,
  'beers'
)

CDN

1. Request

2. HTML + JS

including the inline JSON result

Initial request (retrieving static file)

useStatic

@TheAlexLichter

when your generated site is live

const beer = useStatic(
  slug => axios.get(`https://api.nuxtjs.dev/beers/${slug}`),
  slug,
  'beers'
)

Client-side navigation (CSR)

1. Get JSON from /static/ folder

JSON

2. Return and cache content if request succeeded

If it failed, run the factory function on client-side

onGlobalSetup

@TheAlexLichter

  • Useful for global injects "the Vue way" (provide/inject)
  • Great place for global, layout-independent, composables
  • Mostly used in Nuxt.js plugins
import { provide,onGlobalSetup, } from "@nuxtjs/composition-api";
import { DefaultApolloClient } from "@vue/apollo-composable";

export default defineNuxtPlugin(({ app }) => {
  onGlobalSetup(() => {
    provide(DefaultApolloClient, app.apolloProvider?.defaultClient);
  });
});

useMeta

@TheAlexLichter

manipulate head data easily

import { defineComponent, ref, useMeta } from '@nuxtjs/composition-api'

// component must be defined via defineComponent
export default defineComponent({
  head: {} // has to exist to work
  setup() {
    // Setting some meta data
    useMeta({ title: 'My awesome site' })
  
    // Get refs for meta data
    const { title } = useMeta()
    title.value = 'New title'
  
    // Computed Meta
    const message = ref('')
    useMeta(() => ({ title: message.value }))
    message.value = 'Changed!'
  }
})

Example

Canonical Links

@TheAlexLichter

  • Importan for SEO
  • Avoid duplicate content
  • Define the "preferred URL"

@TheAlexLichter

import { useContext, computed, useRoute } from '@nuxtjs/composition-api'

export function useCurrentUrl () {
  const { $config } = useContext()
  const route = useRoute()

  const { baseUrl } = $config

  return computed(() => baseUrl + route.value.path)
}

useCurrentUrl

@TheAlexLichter

import { useCurrentUrl } from '@/composables/useCurrentUrl'
import { watch, computed } from '@nuxtjs/composition-api'

export function useCanonical (link) {
  const currentUrl = useCurrentUrl()
  const urlWithSlash = computed(() => {
    const url = currentUrl.value
    const suffix = url.endsWith('/') ? '' : '/'
    return url + suffix
  })

  const canonicalLinkIndex = computed(() => link.value.findIndex(l => l.rel === 'canonical'))

  const removeLinkIfExists = () => {
    const doesLinkExist = canonicalLinkIndex.value !== -1
    if (!doesLinkExist) {
      return
    }
    link.value.splice(canonicalLinkIndex.value, 1)
  }

  watch(currentUrl, () => {
    removeLinkIfExists()
    link.value.push({ rel: 'canonical', href: urlWithSlash.value })
  }, { immediate: true })
}

useCanonical

@TheAlexLichter

import { onGlobalSetup, useMeta } from '@nuxtjs/composition-api'
import { useCanonical } from '@/composables/useCanonical'

export default function () {
  onGlobalSetup(() => {
    const { link } = useMeta()
    useCanonical(link)
  })
}

DONE!

New refs!

@TheAlexLichter

ssrRef

@TheAlexLichter

reactive reference with SSR in mind

  • State from the server needs to be transferred from the server to the client (on SSR)
  • ssrRef will do that automatically via window.__NUXT__
  • Also accepts setter function
import { ssrRef } from '@nuxtjs/composition-api'

const setFrom = ssrRef('')
setFrom.value = process.server ? 'server' : 'client'

const expensive = ssrRef(someExpensiveFunction)

There are more!

@TheAlexLichter

  • shallowSsrRef
    • shallowRef + ssrRef
  • reqRef
    • ref that resets itself per request
  • reqSsrRef
    • reqRef + ssrRef

Check out our docs

Gotchas

@TheAlexLichter

Some helpers are using keys under the hood for serialization:

shallowSsrRef, ssrPromise, ssrRef, useAsync

When using them in global composables, ensure that they use a unique key

Gotchas

@TheAlexLichter

function useGetFancyResult() {
    // The key for ssrRef is injected automatically
    // But it is NOT UNIQUE per function call by default
    const result = ssrRef('')
    // ...
    return result
}

const a = useGetFancyResult()
const b = useGetFancyResult()

b.value = 'oh oh!'
// Now, a and b will have the same value
// Because they use the SAME KEY internally
function useGetFancyResult() {
    // The key for ssrRef is injected automatically
    // But it is NOT UNIQUE per function call by default
    const result = ssrRef('', 'ajsiodasjiod') // BABEL
    // ...
    return result
}

const a = useGetFancyResult()
const b = useGetFancyResult()

b.value = 'oh oh!'
// Now, a and b will have the same value
// Because they use the SAME KEY internally

Gotchas

@TheAlexLichter

function useGetFancyResult(key) {
    // The key for ssrRef is injected automatically
    // But it is NOT UNIQUE per function call by default
    const result = ssrRef('', key)
    // ...
    return result
}

const a = useGetFancyResult('a')
const b = useGetFancyResult('b')

b.value = 'no problem!'
// Now, a and b will NOT have the same value
// Because the keys are DIFFERENT

Summary

@TheAlexLichter

  • Nuxt + Composition API is a thing
  • It provides nuxt-specific functions
  • And gives lots of space to build your own composables
  • Give it a try!

Questions

@TheAlexLichter

Q&A after the

next slide!

Thank you!

@TheAlexLichter

Made with Slides.com