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.
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!
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.
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:
window.__NUXT__
object and picked up by your client-side code as its initial state.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>
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>
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
}
}
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.
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 ❤️
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 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.