There are a number of benefits to writing "Universal JavaScript" applications - applications that render full pages on the server but then after page load "hydrate" a single page application. These applications have all of the SEO and initial pageload sped benefites of server-rendered applications, combined with the fluidity and power of a SPA.
In order to realize those benefits of pre-rendering, you need to make certain that your server-side rendering has all of the data it needs before rendering. This is trivial for static pages, but for dynamic applications that depend on API calls, you will need to make sure all critical data is fetched before the page is rendered and sent from the server.
Today I want to break down the hooks provided for this type of asynchronous data fetching in Nuxt.js, a powerful Universal JavaScript Framework built on top of Vue.js.
Why Do We Need Special Hooks?
Before we dive in, let's really quickly ask why we need special hooks at all. In a typical modern SPA application, whether it is built with Vue or React, data is fetched asynchronously, often triggered by lifecycle hooks within the components themselves. A page or component will render in an empty state, kick off an API request to fetch data, and then rerender/update when that data arrives.
The problem with this in a server-side-rendering context is that it is indeterministic. The empty state is just as valid as the non-empty state, and so the server may well simply serve that empty state to the user.
This won't break the user experience, after they get the empty state the component will hydrate on the client, fetch more data, and render just the same. But it negates most of the benefits of the server-side rendering, where the whole point was to send a complete experience in the first pageload.
Nuxt.js Hooks for Asynchronous Data
Nuxt.js has three different hooks explicitly designed for this type of 'asynchronous' data fetches:
- nuxtServerInit: Used to prepopulate the VueX store - called for any page
- fetch: Used to prepopulate the VueX store with data, called from within a page.
-
asyncData: Used to populate the
data
object of a page with synchronous data.
It is also possible to utilize middleware in an asynchronous manner, which means you can use it to populate the VueX store.
The Nuxt documentation provides this visual diagram of how these relate to each other:
According to this diagram, the hooks happen in this order: nuxtServerInit
, middleware
, and then fetch
and asyncData
. Lets break down the details in that order.
nuxtServerInit
This is a hook that Nuxt has inserted into it's initialization process for populating VueX store data that should always be there. It is called on the server only, and is used for populating store data that should be there on every page load.
The way it works is that if your primary store index has defined nuxtServerInit
as an action, it will be run prior to any middleware or other page intialization. It can be synchronous or asynchronous; if it returns a promise Nuxt will wait for that promise to resolve before continuing.
For example, we might use this to populate current user data:
// store/index.js
actions: {
nuxtServerInit ({ commit}, { req }) {
if (req.session.user) {
commit('setUser', req.session.user);
return axios.get(`/users/${req.session.user}`).then((response) =>{
commit('currentUserData', response.data);
})
}
}
}
Note: nuxtServerInit
is only called on your main store, so if you are using modules in your store (and if you have any marginally complicated application you probably are), you will need to chain any setup from there. For example if I wanted to initialized things in both the user module and a 'news' module, I might do:
// store/index.js
actions: {
nuxtServerInit ({ dispatch }, context) {
return Promise.all([
dispatch('user/nuxtServerInit', context),
dispatch('news/nuxtServerInit', context)
]);
}
}
middleware
Middleware lets you define custom functions that run before rendering a page or group of pages. It can be used to guard pages or layouts, for example by checking if a user is authenticated to see them, but it can also be used to fetch asynchronous data. It doesn't have direct access to the page, as that page has not rendered yet, but it can populate the store.
One benefit of middleware is it is reusable - you can apply the same middleware to multiple pages, or a whole group that share a layout. This makes it a very nice place to put data preloading that is shared across a set of pages, but not global across your application like nuxtServerInit
.
The downside of using middleware is that by the time it runs, it is not yet guaranteed the page will render. Later middleware or the validate hook might still prevent the page from rendering, so if your API calls are particularly expensive you might want to save them for later in the lifecycle.
As an example of how we might use middleware, let's imagine that whenever a user is in their "account" area we want to preload a set of settings for them. This might look like:
// layouts/account.vue
export default {
middleware: ['preload-settings']
...
}
// middleware/preload-settings.js
export default function ({ store }) {
if (store.state.settings.settings.length === 0) {
return store.dispatch('settings/loadSettings');
}
return true;
}
This middleware checks to see if the settings
value in the settings module of the VueX store is empty. If so, it dispatches an action to fill it, if not it simply returns true.
So long as that action returns a promise, by returning the result of the dispatch our middleware also returns a promise. Nuxt will wait for that promise to be resolved before continuing, and thus that action can populate our store before rendering.
fetch
The fetch
hook is also used to initialize the VueX store before rendering, but rather than being globally applied to every page, it is page specific. It will not be called if defined on layouts or sub-page components, only within a page.
A fetch
method defined on a page component will be called after all middleware has run and validation has cleared, so by the time it runs we know for certain this page will render. This makes it ideal for fetching expensive data that is necessary for page render but that you wouldn't want to do speculatively.
One quick note: Despite being defined within a page component, it is called before that component is completely initialized, so it does not have access to that component's data, computed attributes, etc. In fact, this
will not refer to the component at all. Instead, the fetch
method is passed the context
object so you can access the store and other needed functionality.
An example of using the fetch method to fetch a specific product's info into the store:
// pages/products/_id.vue
export default {
fetch(({ store, params }) {
if (typeof (store.state.products.byId[params.id]) === 'undefined') {
return store.dispatch('products/loadProduct', {id: params.id});
}
}
...
}
asyncData
Up until this point, all of the mechanisms we've covered have been focused on populating data into the VueX store. But sometimes you don't need (or want) the store, you just want to put data into your component's data object.
Nuxt has you covered here too, at least for within a page component, with the asyncData
hook. This hook will be called prior to rendering a page, and the object it returns will be merged with the data
object for your component.
For example, if for some reason we didn't want to use the VueX store in our previous example to hold product data - perhaps we want to make sure it is always 100% up to date and so refetch it every time the product page is viewed. We could implement it this way:
// pages/products/_id.vue
export default {
asyncData(context) {
return axios.get(`https://my-api-server/api/products/${params.id}, (response) => {
return { product: response.data };
});
}
...
}
Similar to fetch
, asyncData
is called prior to the component being fully initialized, so it does not have access to the component instance itself, and is passed the context
object to access any app-level information it needs.
And there we have it. The 4 mechanisms Nuxt.js provides for populating asynchronous data prior to render.
P.S. - If you're interested in these types of topics, you should probably follow me on Twitter or join my mailing list. I send out a weekly newsletter called the ‘Friday Frontend’. Every Friday I send out 15 links to the best articles, tutorials, and announcements in CSS/SCSS, JavaScript, and assorted other awesome Front-end News. Sign up here: https://zendev.com/friday-frontend.html
Top comments (0)