22 May 2023

nuxtServerInit: where is it in Nuxt 3

If you're a Nuxt 2 user, you're probably familiar with the concept of server-side rendering (SSR). Nuxt 2 came with a powerful feature called nuxtServerInit - which was used to initialise default state on the server-side before rendering the application. In this blog post, we'll take a closer look at what happened to nuxtServerInit in Nuxt 3.

nuxtServerInit in Nuxt 2

Here’s a little back story for those who doesn’t have prior experience with Nuxt 2.

nuxtServerInit came fourth in line to be called after 1) Nuxt modules, 2) server-middleware and 3) Nuxt plugins. nuxtServerInit was tightly coupled with Vuex store because Vuex was the preferred state management solution for Nuxt 2. nuxtServerInit was written within actions object of the Vuex store👇

// store/index.js
actions: {
  nuxtServerInit ({ commit }, { req }) {
    if (req.session.user) {
      commit('user', req.session.user)
    }
  }
}

Within nuxtServerInit, we could fetch the initial data from an API endpoint - mostly using axios - and then use mutation to set the data in the Vuex state. This initial data was then shared across all pages of our Nuxt application.

To summarise, nuxtServerInit :

  • worked with an SSR
  • worked alongside state management solution
  • fetched data from the server-side, and set that data on the client-side
  • facilitated initial data for every HTTP requests made to the page, no matter which page was accessed first
  • allowed data access in Nuxt pages and Vue components

nuxtServerInit in Nuxt 3

Unfortunately, we have lost nuxtServerInit in Nuxt 3. However, Nuxt 3 provides multiple ways to achieve the same functionality as nuxtServerInit with even more freedom to customise depending on the size of the project.

We have two main ways to achieve this: 1) with Pinia or 2) using useState.

In both options, we will have a common server-side event handler to fetch the data. Let's say we have a list of blog posts as the default data for our Nuxt application. As an example, we will fetch list of posts from an API endpoint👇

// server/api/post.ts

import axios from 'axios'

export default defineEventHandler(async (event) => {
  try {
    const response = await axios.get('https://dummyjson.com/posts')
    const data = response ? response?.data?.posts : []
    return data
  } catch (error) {
    console.log('error in fetching posts')
  }
})

Pinia

To begin with this option, we will create Pinia store - store/post.ts with:

  • an empty state data,
  • an action to fetch the data from an /api/post endpoint we created in the previous step and set the data into state, i.e. this.data👇
// store/post.ts

export const usePostStore = defineStore('post-store', {
  state: () => ({
    data: [] as Post[],
  }),
  actions: {
    /**
     * Fetch Posts
     * @returns array of posts of type Posts[]
     */
    async get() {
      try {
        const posts = await useFetch<Post[]>('/api/post')
        this.data = posts.data ? posts.data.value as Post[] : []
      } catch (error) {
        console.log('error in fetching posts')
      }
    }
  }
})

From this point onwards, we have again two options to provision this data from server to client using Pinia.

  • Option 1. Invoke the Pinia action in Nuxt plugin OR
  • Option 2. Invoke the Pinia action in the project root, in the setup function of app.vue

Option 1: Pinia with Nuxt Plugin

We will create one server-side Nuxt plugin init.pinia.server.ts and call the Pinia action using useAsyncData. This way post.get() function can abstract any complex data-fetching logic that it may have👇

// plugins/init.pinia.server.ts

import { usePostStore } from '~/store/post'

export default defineNuxtPlugin(async (nuxtApp) => {
  const posts = usePostStore(nuxtApp.$pinia)
  await useAsyncData('posts', () => posts.get())
})

This plugin will run right before the Nuxt page lifecycle begins, making sure that we can access the data on Nuxt pages, components, composable and any Nuxt plugins that are called after this one. And finally, we only need to call the Pinia state to see the actual data👇

// pages/Posts.vue
import { usePostStore } from '~/store/post'
const post = usePostStore()
// console.log(post.data)

Nuxt plugin option is useful when any of the other Nuxt plugins need to use this server-side data, because Nuxt 3 allows us to control the order in which Nuxt plugins are called by prefixing with alphabetical numbering to the file names, i.e. 01.init.pinia.server.ts And now we can be sure that the initial data (posts in this examples) is available to any other plugins.

Option 2: Pinia with app.vue

When we don’t want to use server-side plugin to do the initial data-fetching, Nuxt 3 provides us app.vue file in the root of the Nuxt project that is called every time we make an HTTP request to the Nuxt application. This makes app.vue a perfect place to fetch the initial data. We can use useAsyncData to call the exact same action from our Pinia store, but this time in app.vue👇

// app.vue

<script setup>
import { usePostStore } from '~/store/post'
const post = usePostStore()

// when not using server-side plugin to load data
await useAsyncData('posts', () => post.get())
// console.log(post.data)
</script>

This gives similar results as the plugin we made earlier. The only difference in this approach is that all the plugins have already been called before we reach at app.vue. This approach works fine as long as any other Nuxt plugins do not require the initial data in your Nuxt application.

useState

This option does not require any external dependencies because useState composable comes packaged with Nuxt 3.

We have again two options to provision this data from server to client-side using useState.

  • Option 3. useState with Nuxt plugin
  • Option 4. useState in the project root, in the setup function of app.vue

Option 3: useState with Nuxt Plugin

We can make use of the same API endpoint we created earlier, then invoke the event handler using useFetch and finally store the data in useState using server-side Nuxt plugin👇

// plugins/init.usestate.server.ts

const usePost = () => useState<Post[] | null>('posts', () => {
  const posts = useFetch<Post[]>('/api/post')
  return posts?.data || null
})

export default defineNuxtPlugin(() => {
  const posts = usePost()
})

Once the plugin is setup, we can use the ‘post’ key as defined above to access this data in Nuxt pages, components or composable👇

// app.vue
<script setup>
    const posts = useState('posts')
    // console.log(posts.value)
</script>

Option 4: useState with app.vue

When we don’t want to use server-side plugin to do the initial data-fetching, we can define useState function directly in app.vue at the root of the Nuxt project, then invoke the event handler and store the data in useState as below👇

// app.vue

<script setup>
const usePost = () => useState<Post[] | null>('posts', () => {
  const posts = useFetch<Post[]>('/api/post')
  return posts?.data || null
})
const posts = usePost()
// console.log(posts.value)
</script>

In this instance, we will not have to use the ‘posts’ key to access the data. Instead, we can start using posts variable to access the data right inside app.vue

‘posts’ key will be needed however, if we want to access the data elsewhere, i.e. on Nuxt pages, components or composable as below👇

// components/Posts.vue OR 
// pages/posts.vue
<script setup>
  const posts = useState('posts')
  // console.log(posts.value)
</script>

Github:

Here's the working examples of both Pinia and useState on Github. Check out the /plugins, /store, /server directories and app.vue file.

[Nuxt 3 Lessons > Branch: nuxt-server-init ](https://github.com/Krutie/nuxt-3-lessons/tree/nuxt-server-init) on Github

Conclusion

nuxtServerInit was a powerful feature in Nuxt 2 that was used to initialise default state of the data on the server-side before rendering the application. We have covered overall four ways to achieve the same functionality in Nuxt 3:

  1. Pinia with Nuxt Plugin,
  2. Pinia with app.vue,
  3. useState Nuxt Plugin and
  4. useState with app.vue

This functionality is useful to set up some initial state that is shared across all pages, components, composable and plugins (in cases where plugins are used) of the Nuxt application. If you're nuxtifying your Vue.js application with server-side rendering, Pinia and useState are definitely worth checking out.

In conclusion, Nuxt plugins are a great feature that can help you abstract common, repetitive tasks to improve the performance and user experience of your Nuxt applications, while app.vue can be an easy starting point to do some complex tasks.