Banner
Home / Blog / Nuxt Composition API
Nuxt Composition API

Nuxt Composition API

Filip Rakowski
Filip Rakowski
August 26th 2020

In the last couple of months Composition API took Vue community by storm. Thanks to a plugin that brings it’s capabilities to Vue 2 the new API is already supported in many of the most impactful projects from Vue ecosystem like Vuex vue-router or vue-apollo. Unfortunately, despite a very quick adoption Composition API was lacking a proper way of supporting applications that are utilizing Server-Side Rendering. The majority of such applications is written in NuxtJS. The Nuxt core team was very aware of that issue and few months after Composition API plugin for Vue 2 was released they did what they’re doing best - made an awesome module to help their community. In this article I want to introduce @nuxtjs/composition-api module that brings first-class Composition API support to Nuxt.

Disclaimer: Nuxt Composition API module (as well as Vue Composition API Plugin) is experimental and could be unreliable therefore it’s not recommended for production usage. The purpose of this article is to show some interesting possibilities that Composition API could bring to Nuxt.

Issues with Vue 2 Composition API plugin

So what exactly made Vue 2 Composition API plugin not suitable for Nuxt?

First of all, most of the Nuxt features like fetch , head or asyncData were available only through Options API. Because of that it was hard to benefit from Composition API biggest value - ability to encapsulate all code related to a certain feature within a composable. Most of the Nuxt component APIs concentrate around server-side data fetching and conveying it to client-side so their lack was a major issue, especially for library authors willing to make their Composition API ports compatible with Nuxt.

One of the key arguments behind using Vue 2 Composition API plugin was a promise of writing a future-proof code that could be easily migrated to Vue 3. This promise, obviously could not be delivered when half of the code still had to be written inside the components.

Thankfully we had to wait only few months to see a @nuxtjs/composition-api module that resolved above issues!

Nuxt Composition API

The @nuxtjs/composition-api package is a wrapper over a @vue/composition-api plugin which means that along with Nuxt-specific utilities it contains all “standard” Composition API functions like ref or computed.

The installation is straightfoward, just like with every other Nuxt module. First we have to install the package:

npm install @nuxtjs/composition-api --save

and register the module in our nuxt.config.js

{
  buildModules: [
    '@nuxtjs/composition-api'
  ]
}

The module is registering @vue/composition-api plugin under the hood so we can immediately use its features in our project without any additional code.

Dealing with server-side data

I mentioned earlier that without a Nuxt wrapper it was hard to utilize it’s SSR capabilities within composables. Let’s see what issues we could run into if we use a standard @vue/composition-api plugin without Nuxt enhancements. Take a look at below composable:

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

function usePost () {
  const post = ref({})

  const fetchPost = async (id) => {
    fetch('https://jsonplaceholder.typicodcom/posts/' + id)
      .then(response => response.json())
      .then(json => post.value = json)
  }

  return {
    post,
    fetchPost
  }  
}

Now lets see what will happen when we use this composable in a Nuxt app (Universal mode)

<template>
  <div>{{ post.title }}</div>
</template>

<script>
import { usePost } from '@/composables'

export default defineComponent({
  setup (props, { root }) {
    const { post, fetchPost } = usePost($root.route.params.id)

    fetchPost()

    return { post }
  }
})
</script>

There are two major issues we will run into:

If you check the server-side output (right-click on the app and View source) you will see that it rendered an empty div:

<div></div>

Even though we expected to see a post title it was never returned from the server therefore crawlers (like Google bot) most likely won't be able to properly index our website. It happened because fetching is asynchronous action and our server-side code run synchronously. We never “asked” it to wait for this asynchronous call to finish.

Another issue we will run into can be observed in the Network tab of our dev tools. Even though the fetching code was executed on the server side and (in theory) we should already have the content of the post (we're not, we will fix that in a moment) its being fetched on the client-side again. Because of that all our asynchronous network calls are doubled. It is rather common issue observed in Server-Side rendered SPAs.

Thankfully both issues can be fixed by one function - useFetch which is a Composition API wrapper for a well-known Nuxt fetch that is commonly used to fetch asynchronous data (both on a server and client side)

It does two things:

  • It makes sure that the asynchronous function that was passed as its argument was resolved. Then the result is conveyed into client-side through window.__NUXT__ object and picked up by your client-side code as its initial state.
  • Its preventing the client-side execution if navigation has been made on a server-side. Because of that we’re not performing unnecessary network calls if we already have the data from the server side fetch.

To make use of useFetch in our composable we just need to wrap it around our asynchronous code:

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

function usePost (id) {
  const post = ref({})

  const { fetch: fetchPost } = useFetch(async () => fetch('https:jsonplaceholder.typicode.com/posts/' id)
    .then(response => response.json())
    .then(json => post.value = json)
  )

  return {
    post,
    fetchPost
  }  
}

useFetch is returning a fetch function (renamed to fetchPost) that we will use to make the asynchronous call.

Now the server-side rendered code contains our posts title and we’re not doing any redundant network calls!

<div data-fetch-key="0"> sunt aut facerrepellat provident occaecati excepturoptio reprehenderit </div>

Side note - how conveying the server-side data works?

If you want to understand better how the above code and useFetch are working you should take a look at the server-side rendered HTML. If you scroll down to the bottom of the file you will notice a declaration of window.__NUXT__ object.

Nuxt is using this object to convey the data obtained on the server side to the client side. Once the hydration happens (which means Vue is taking control over server-side rendered HTML) the components are picking up the server-side state from this object. Without it we would always have to make an additional network call as server-side content would be replaced with default values of reactive Vue properties that we have statically declared in our code. In our case post will become an empty object even though it contains the post title on the server-side rendered HTML.

useFetch and fetch methods are using a window.__NUXT__.fetch property to store the data that was fetched on the server-side. Below you can see an example of window.__NUXT__ object from the above app.

<script>window.__NUXT__=(function(a){retur{layout:"default",data:[{}],fetch:[{pos{userId:a,id:a,title:"sunt aut facerrepellat provident occaecati excepturoptio reprehenderit",body:"quia esuscipit\nsuscipit recusandae consequuntuexpedita et cum\nreprehenderit molestiae uut quas totam\nnostrum rerum est autem sunrem eveniet architecto"}}],error:nulserverRendered:true,routePath:"\u002Fconfig:{},ssrRefs:{},logs:[]}}(1));</script>

Using Nuxt Context in your composables

Along with data-related server-side capabilities, Nuxt Composition API is delivering a set of composables to interact with its other APIs. One of the most useful ones is useContext.

useContext, as name suggests is giving us access to Nuxt Context inside our composables. We can use it to access our router instance, store but also to access properties added via Nuxt Modules.

In our app we can use useContext to utilize @nuxtjs/http module instead of the native fetch function.

First we have to install the module itself:

yarn add @nuxt/http

and add it to our nuxt.config.js

modules: [
  '@nuxt/http',
],
http: {
  // options
}

Now we can access it through $http property of Nuxt Context. Lets upgrade the fetchPost method of out usePost composable to use @nuxt/http:

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

export const usePost = (id) => {
  const post = ref({})
  const { $http } = useContext()

  const { fetch: fetchPost } = useFetch(async () =>
    post.value = await $http.$get('https://jsonplaceholder.typicode.com/posts/' + id)
  )

  return {
    post,
    fetchPost
  }  
}

Handling meta tags with useMeta

Another very useful composable that comes with Nuxt Composition API is useMeta. As you probably have guessed its a wrapper for a head function and allows us to control the meta tags from within composables and the setup method.

We have to somehow execute it inside our useFetch function - otherwise the post data won’t be there yet and render undefined. To make it work we can create a hook inside our usePost that will execute some additional code inside useFetch.

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

export const usePost = (id) => {
  const post = ref({})
  const { $http } = useContext()
  const toExecute = []  
  const { fetch: fetchPost } = useFetch((async () => {
    post.value = await $http.$get('https://jsonplaceholder.typicode.com/posts/' + id)
    await Promise.all(toExecute.map(cb => cb()))
  })

  return {
    post,
    fetchPost,
    onFetchPost: fn => toExecute.push(fn),
  }  
}

Let’s quickly review what happened here. First we’re creating an array where we will put the functions that we want to execute within usefetch

const toExecute = []

Next, inside useFetch we’re executing all these functions

    await Promise.all(toExecute.map(cb => cb()))

At the end we’re returning onFetchPost hook which pushes a passed function into toExecute array.

Voila! Now we can use our composable with useMeta inside the setup function:

export default defineComponent({
  head: {},
  setup () {
    const { post, fetchPost, onFetchPost } = usePost(1)
    const { title } = useMeta()

    fetchPost()

    onFetchPost(async () => {
      title.value = post.value.title
    })

    return {
      post
    }
  }
})

Side note: the head property is required for useMeta to work.

Summary

Nuxt Composition API is a powerful tool that allows its users to utilize Nuxt superpowers together with Composition API flexibility. Thanks to this we can organize our code better and because of that maintain our codebases easier.

Big thanks to Pooya from Nuxt core team for the tips ❤️

Start learning Vue.js for free

Filip Rakowski
Filip Rakowski
Co-founder & CTO of Vue Storefront - Open Source eCommerce Platform for developers. I'm passionate about sharing my knowledge and thoughts on software architecture, web performance, Vue and raising awaraness about business impact of the technology and code we write. When I'm not working I'm probably playing with one of my cats.

Latest Vue School Articles

What Makes a Good (Vue) Component Library? A Checklist

What Makes a Good (Vue) Component Library? A Checklist

Considering building a Vue Component library? Checkout this checklist of concerns you should make sure to remember!
Daniel Kelly
Daniel Kelly
How to fit 1100+ lessons of free Vue.js training into 48 hours

How to fit 1100+ lessons of free Vue.js training into 48 hours

Vue School’s Free Weekend is back from 23-24 March 2024. This how-to guide will show you how to make the best use of the free Vue.js training courses on offer.
Maria Panagiotidou
Maria Panagiotidou

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 120.000 users have already joined us. You are welcome too!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.