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

Nuxt.js and the Composition API (Developer Group Leipzig 2021)

By Alexander Lichter

Nuxt.js and the Composition API (Developer Group Leipzig 2021)

Vue 3 and the Composition API are out for a bit now. And while the ecosystem is catching up, more and more libraries provide support for the Composition API through composables. But what is about Server-Side Rendering with the composition API, especially when using Nuxt.js? I got you covered! In this talk, you will learn about the @nuxtjs/composition-api package that provides you a couple of nifty composables as well as SSR support in conjunction with the Composition API itself. Together we look into the new additions of the packages as well as several use cases and examples of how to use the Composition API with Nuxt.js.

  • 1,471