Nuxt data-fetching techniques: Which to use and when

Nuxt data-fetching techniques: Which to use and when

20 Jan 2022 nuxt vue vuex

Nuxt is a fantastic option for building web projects, whether it's a full-on site or a single page app. Whatever you're building, there's a good chance that it'll need to retrieve data over AJAX at some point, e.g. from a third-party API.

Fortunately, Nuxt provides a couple of ways to do this, and in this article I'll cover how those ways compare and which to choose when, as well as looking at some alternative approaches.

The fetch hook 🔗

The fetch hook is the most versatile of Nuxt's data retrieval methods. It can be used in any component, can run server- or client-side (more on that later) and can be re-triggered later after the component has mounted. For this reason, it has access to the component's this object.

This makes it perfect, say, for fetching data intended for an AJAX-populated HTML table, which may need to be re-populated later if the user interacts with filters or sort controls.

The fetch hook should return a promise, either explicitly, or implicitly via async/await.

export default { data() { return { //... }}, async fetch() { let url = 'https://api.nuxtjs.dev/mountains'; this.mountains = await this.$http.$get(url); } }

Examples in this article use Nuxt's HTTP module, but we could just as easily use the standardised Fetch API.

Once we've retrieved our data, it's up to us what we do with it. Normally, as above, we'd store it somewhere on the component's data - in our case, in an object called mountains, which our component template can then read from to output the table content via a v-for loop.

<template> <table> <tr v-for='mountain in mountains'> ... </tr> </table> </template>

If we're using Vuex, we may prefer to commit it to our state object:

async fetch() { let url = 'https://api.nuxtjs.dev/mountains'; let mountains = await this.$http.$get(url); this.$store.commit('mountains', mountains); }

Fetch hook considerations 🔗

As versatile as the fetch hook is, there's a few considerations when using it.

First, at what stage in Nuxt's lifecycle and in what environment (server or client) does it run? This depends on the value of the ssr (server-side rendering) param in your Nuxt config:

  • If it's set to true, the fetch hook will run on the server - either pre-render if you're using target: 'server' or during static site generation (SSG) if you're using target: 'static'.
  • If it's set to false, it will run on the client, on route load

If you're using ssr: true but for some reason want the fetch hook to run on the client, not the server, you can force this via the fetchOnServer component param.

It's also possible to set the minimum execution time the fetch hook will take, via fetchDelay component param.

export default { //... fetchDelay: 500 //milliseconds; default = 200 }

This is useful to avoid quick flashes of empty-then-populated content when your request resolves quickly. You can use this time to show a loading spinner, for example.

Also note that, by default, the fetch hook is not called when the query string changes. This can be changed by mapping the hook to the $route.query within the component's watch object.

export default { data() { return { //... }}, watch: { '$route.query': '$fetch' } }

You can also tell the fetch hook to listen to query string changes via the watchQuery property, but this involves more overheads. We'll meet this later when we look at the asyncData hook.

The fetch state 🔗

The fetch hook comes with a handy means of interrogating the request state via the this.$fetchState object. This contains three members:

  • pending - a boolean that denotes whether the fetch hook's promise has resolved yet (client-side only - i.e. if ssr: false or fetchOnServer: false)
  • error - if the fetch hook throws an error, it can be accessed via this property
  • timestamp - the timestamp of the last time the fetch hook ran. Useful for implementing caching strategies

Like everything in Vue, we can use these bits of info in our reactivity. The pending property is particularly useful; it allows us to show a loading spinner or wait message to the user until the data is fetched.

<template> <div v-if='$fetchState.pending'>Please wait...</div> <div v-else-if='$fetchState.error'>Oh no!</div> <div v-else> ... </div> </template>

The asyncData hook 🔗

The asyncData hook fetches data just like the fetch hook does, but with three important differences:

  • It can be used only in page components
  • It can merge response data straight into page component data
  • It cannot access the component's this instance

asyncData happens on route change, and is resolved before the page is rendered. This is why you don't have access to this, because it doesn't exist at the time asyncData runs.

Instead, the hook is passed the Nuxt context as its only argument so you can still access route info, Nuxt modules etc.

Let's fetch our mountains again, but this time via the asyncData hook.

export default { async asyncData() { let url = 'https://api.nuxtjs.dev/mountains' let mountains = await this.$http.$get(url); return {mountains}; } }

See how we return the response? As far as our page component is concerned, when it came to life it already had a mountains definition in its data, because asyncData merged into it.

Refreshing asyncData 🔗

We saw earlier that, with the fetch hook, it's possible to refresh the fetch data simply by calling this.$fetch() at a later time after the component is mounted.

But this won't work with asyncData. Instead, we can either:

  • Refresh the page - but this will mean hitting the server again; or, even better...
  • ...call this.$nuxt.refresh()

the refresh() method of the Nuxt context refreshes re-renders the current page's components and, crucially for our needs, re-fires any asyncData or fetch hooks. But if you find yourself often needing to refresh asyncData, you probably should be using fetch or some other means instead. asyncData is primarily intended to pump initial data into the page.

The asyncData hook, by default, treats route query string changes the same way the fetch hook does - i.e. it doesn't respond to them unless you explicitly tell it to. This looks a little different from how we told the fetch hook to re-fire on query string changes above, and involves setting the watchQuery component property:

export default { watchQuery: ['someQSParam'] //or true for all }

When the value of the someQSParam query string param changes value, the component's methods (including fetch() and asyncData()) will be re-fired.

Other approaches 🔗

We've looked at the two main approaches in Nuxt to fetch data. But as ever with code, there are alternatives.

Vuex actions 🔗

First up, there's Vuex actions. It's common to handle asynchronous data retrieval within state management modules such as Vuex, and Vuex provides actions as a way to handle this.

Unlike mutations, actions can be asynchronous, which means they're perfect for fetching data. The basic flow is to fetch the data in an action, then commit it to a mutation.

//store/index.js export const state = () => ({ mountains: [] }); export const mutations = { setMountains(state, mountains) { state.mountains = mountains; } } export const actions = { async setMountians(context) { let url = 'https://api.nuxtjs.dev/mountains'; let mountains = await this.$http.$get(url); context.commit('setMountains', mountains); } }

We can then 'dispatch' the action from our component when we want to get the data.

I mention this method only in passing. This isn't a tutorial on Vuex, so to learn more here's how Nuxt implements Vuex, and here's the Vuex docs themselves.

Router middleware 🔗

Secondly, we could use router middleware to fetch our data. Middleware runs before components are created, so isn't able to store the response data on the component. Instead, its only option is to store the data in state.

Suppose we wanted to get the latest mountain data for each and every page visit. We can stipulate middleware rules at any of three points in the Nuxt lifecycle:

  • in the config (to match certain routes)
  • in layout files (pages of a certain layout only)
  • in pages (that page only)

We want our middleware to run on all visits to all pages, so we'll set up a catch-all rule in the config.

//nuxt.config.js export default { router: { middleware: 'getMountains' } }

That tells Nuxt to retrieve and run the main export function in the file /middleware/getMountains.js on every page visit. In there, we can do our request:

//middleware/getMountains.js export default async ctx => { let url = 'https://api.nuxtjs.dev/mountains'; let mountains = await ctx.$http.$get(url); ctx.store.commit('setMountains', mountains); }

Et voila - our mountains data is now available everywhere just by reading the state.

Summary 🔗

Nuxt provides two main hooks for fetching asynchronous data - fetch() and asyncData().

Use the fetch hook:

  • In any component, not just page components
  • When you need to refresh the initial fetch data later, e.g. in response to user input

Use the asyncData hook:

  • In page components only
  • To merge fetched data into the page's data

Alternatives including fetching data in Vuex actions or in router middleware.

Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!