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 2Here’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
:
nuxtServerInit
in Nuxt 3Unfortunately, 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')
}
})
To begin with this option, we will create Pinia store - store/post.ts
with:
data
,/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.
app.vue
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.
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.
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
.
useState
with Nuxt pluginuseState
in the project root, in the setup function of app.vue
useState
with Nuxt PluginWe 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>
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
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:
app.vue
,useState
Nuxt Plugin anduseState
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.