DEV Community

Cover image for VueJS - Reusable Data Fetcher Component
Pablo Veiga
Pablo Veiga

Posted on • Edited on

VueJS - Reusable Data Fetcher Component

You are probably able to count on the fingers the number of web applications around the world that do not need to fetch remote data and display it to the user.

So, assuming that your next Single Page Application (written using VueJS 😍) will require external data fetching, I would like to introduce you to a component that will help you manage the state of other components that require data fetching and easily provide proper feedback to the users.

First things first

Initially, it is important to think about how rendering the correct state in your application is useful so that users know exactly what is happening. This will prevent them from thinking the interface has frozen while waiting for data to be loaded and also provide them, in case of any errors, with prompt feedback that will help in case they need to contact support.

Loading / Error / Data Pattern

I am not sure if this is an official pattern (please comment below if you know any reference) but what I do know is that this simple pattern helps you to organize the state of your application/component very easily.

Consider this object. It represents the initial state of a users list:

const users = {
  loading: false,
  error: null,
  data: []
}
Enter fullscreen mode Exit fullscreen mode

By building state objects like this, you will be able to change the value of each attribute according to what is happening in your application and use them to display different parts at a time. So, while fetching data, you set loading to true and when it has finished, you set loading to false.

Similarly, error and data should also be updated according to the fetching results: if there was any error, you should assign it to the error property, if not, then you should assign the result to the data property.

Specializing

A state object, as explained above, is still too generic. Let´s put it into a VueJS application context. We are going to do this by implementing a component and using slots, which will allow us to pass data from our fetcher component to its children.

As per VueJS docs:

VueJS implements a content distribution API inspired by the Web Components spec draft, using the <slot> element to serve as distribution outlets for content.

To start, create a basic component structure and implement the users state object as follows:

export default {
  data() {
    return {
      loading: false,
      error: null,
      data: null
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, create the method responsible for fetching data and update the state object. Notice that we have implemented the API request in the created method so that it is made when the component is fully loaded.

import { fetchUsers } from '@/services/users'

export default {
  data() {
    return {
      loading: false,
      error: null,
      data: []

    }
  },
  created() {
    this.fetchUsers()
  }
  methods: {
    async fetchUsers() {
      this.loading = true
      this.error = null
      this.users.data = []

      try {
        fetchUsers()
      } catch(error) {
        this.users.error = error
      } finally {
        this.users.loading = false
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The next step is implementing the template that will display different things according to Loading, Error and Data states using a slot to pass data, when present, to children components.

<template>
  <div>
    <div v-if="users.loading">
      Loading...
    </div>
    <div v-else-if="users.error">
      {{ users.error }}
    </div>
    <slot v-else :data="users.data" />    
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

With the fetcher component built, let´s use it in our UsersList component.

<template>
   <UsersFetcher>
     <template #default="{ data }">
       <table>
         <tr>
           <th>ID</th>
           <th>Name</th>
           <th>Age</th>
         </tr>
         <tr v-for="user in data" :key="user.id">
           <td>{{ user.id }}</td>
           <td>{{ user.name }}</td>
           <td>{{ user.age }}</td>
         </tr>
       </table>
     </template>
   </UsersFetcher>
</template>
Enter fullscreen mode Exit fullscreen mode
import UsersFetcher from '@/components/UsersFetcher'

export default {
  name: 'UsersList',
  components: {
    UsersFetcher
  }
}
Enter fullscreen mode Exit fullscreen mode

Making the component reusable

That was a very simple approach to implementing the Error / Loading / Data pattern to provide proper feedback to the users when fetching external data, but the implementation above is not very reusable since it is strictly fetching users. By implementing a few changes to our fetcher component, we will make it more generic and we will be able to reuse it for any data fetching we need in our application.

First, let´s make the fetcher component more dynamic since we need to fetch not only users in our application but all kinds of data that require different service methods and variables' names.
In order to do that, we will make use of props to pass dynamic content to the component.

<template>
  <div>
    <div v-if="loading">
      Loading...
    </div>
    <div v-else-if="error">
      {{ error }}
    </div>
    <slot v-else :data="data" />    
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
export default {
  name: 'Fetcher',
  props: {
    apiMethod: {
      type: Function,
      required: true
    },
    params: {
      type: Object,
      default: () => {}
    },
    updater: {
      type: Function,
      default: (previous, current) => current
    },
    initialValue: {
      type: [Number, String, Array, Object],
      default: null
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Analyzing each one of the props above:

apiMethod [required]: the service function responsible for fetching external data

params [optional]: the parameter sent to the fetch function, if needed. Ex.: when fetching data with filters

updater [optional]: a function that will transform the fetched result if needed.

initialValue [optional]: the initial value of the attribute data of the state object.

After implementing the required props, let´s now code the main mechanism that will allow the component to be reused. Using the defined props, we are able to set the operations and control the component's state according to fetching results.

<template>
  <div>
    <div v-if="loading">
      Loading...
    </div>
    <div v-else-if="error">
      {{ error }}
    </div>
    <slot v-else :data="data" />    
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
export default {
  name: 'Fetcher',
  props: {
    apiMethod: {
      type: Function,
      required: true
    },
    params: {
      type: Object,
      default: () => {}
    },
    updater: {
      type: Function,
      default: (previous, current) => current
    },
    initialValue: {
      type: [Number, String, Array, Object],
      default: null
    }
  },
  data() {
    return {
      loading: false,
      error: null,
      data: this.initialValue
    }
  },
  methods: {
    fetch() {
      const { method, params } = this
      this.loading = true

      try {
        method(params)
      } catch (error) {
        this.error = error
      } finally {
        this.loading = false
      }
    }
  } 
}
Enter fullscreen mode Exit fullscreen mode

So, after implementing these changes, this is how we would use the new Fetcher component.

<template>
   <Fetcher :apiMethod="fetchUsers">
     <template #default="{ data }">
       <table>
         <tr>
           <th>ID</th>
           <th>Name</th>
           <th>Age</th>
         </tr>
         <tr v-for="user in data" :key="user.id">
           <td>{{ user.id }}</td>
           <td>{{ user.name }}</td>
           <td>{{ user.age }}</td>
         </tr>
       </table>
     </template>
   </Fetcher>
</template>
Enter fullscreen mode Exit fullscreen mode
import Fetcher from '@/components/Fetcher'
import { fetchUsers } from '@/services/users'

export default {
  name: 'UsersList',
  components: {
    Fetcher
  },
  methods: {
    fetchUsers
  }
}
Enter fullscreen mode Exit fullscreen mode

So, that´s it. Using basic VueJS concepts such as props and slots we were able to create a reusable fetcher component that can be responsible for retrieving data from your API and provide proper feedback to the users of your application.
You can use it more than once on one page and fetch different data as needed.

You can find a fully-working example of this implementation in this repo.

I hope you liked it. Please, comment and share!

Special thanks to @scpnm for helping me to fix an incorrect piece of code in this article.

Cover image by nordwood

Top comments (6)

Collapse
 
gmeral profile image
gmeral • Edited

Hi Pablo !
Thanks for the inspiration and the detailed article.
To me the biggest shortcoming of this implementation is that you do not control when the data is fetched (on your github repo, it is always done on created).

To circumvent this i thought about using a Promise as a prop instead of the apiMethods params and updater (this also makes the component simpler and much more flexible)

Then changing the promise props acts as a reload (or could eventually load something different, it is up to the user). The promise prop can also be omitted if the fetching has to occur later than the component creation (in response to a user action for example)

Here is how it would look like :

<template>
  <div>
    <div v-if="loading">
      Loading...
    </div>
    <div v-else-if="error">
      {{ error }}
    </div>
    <slot v-else :data="data" />
  </div>
</template>

<script>
export default {
  props: {
    promise: {
      type: Promise
    },
    initialValue: {
      type: [Number, String, Array, Object],
      default: null
    }
  },
  data() {
    return {
      loading: false,
      error: null,
      data: this.initialValue
    }
  },
  watch: {
    promise: {
      immediate: true,
      handler(newVal) {
        if(!newVal) return
        this.loading = true
        newVal
          .then(result => this.data = result)
          .catch(error => this.error = error)
          .finally(() => this.loading = false)
      }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vcpablo profile image
Pablo Veiga

Hey @gmeral thanks for commenting. You are right, in the current implementation, the request will be executed each time the component is created.

Your suggestion is pretty good.
The only thing I'd do there is changing the promises callbacks to async/await

In my personal projects, I've done something similar to your implementation.
But I pass a boolean intermediate prop combined with a params watcher.
Like this:

created() {
    const { immediate } = this
    this.watcher = this.$watch('params', this.fetch, { immediate })
 }
Enter fullscreen mode Exit fullscreen mode

The difference is that this doesn't allow me to make different requests but the same one with different parameters.

I kind of don't see with good eyes scenarios where you might populate the same variable and context with different requests results, but it might happen.

Collapse
 
scpnm profile image
Neil Merton

Thanks for the article. Quick question though, where is initialState being set on this line: data: this.initialState?
Should it be data: this.initialValue as set in the props collection?

Collapse
 
vcpablo profile image
Pablo Veiga

You are right @scpnm !
Thanks a lot!

I've fixed it!

Collapse
 
horaciosystem profile image
Horacio Alexandre Fernandes • Edited

Hello Pablo! Very good article, pretty well detailed and I'm sure it will give a lot of insights to someone reading it.
An improvement I'd like to suggest to this API is using a 'status' variable, an enum, with the possible states (thinking in a finite state machine) to avoid some gaps (impossible states) that can occur and can make it trickier to deal with.
I'd like to recommend this article: kentcdodds.com/blog/stop-using-isl... that complies a good explanation for why.
Anyway, thank you so much for the article.

Collapse
 
schleidens profile image
Schleidens.dev

Yayyyy :) this article is helpful 🤗