Vue - Keeping Components Reusable
To make Vue components reusable, there are a few things that you need to watch out for. Trying to understand the concept of components is usually the first hurdle that many developers face when beginning their Vue journey. But what many developers fail to realize when learning how to create components is the amount of time they can save you when used correctly and the amount of headache they can bring you when used incorrectly.
Now, there is never an absolute right or wrong way; sometimes the right way can be the wrong way in certain scenarios and vice versa. Every situation is unique. However, some general guidelines will certainly help keep you from pulling your hair out when it comes time to reuse some of those components.
If you’re not familiar with the concept of presentational and container components, I’ll give you a brief definition of what these are:
- container (parent) - these types of components are meant for managing data, such as calling APIs and passing the data to presentational components.
- presentational (child) - these types of components are meant simply for displaying data and are designed to be reused repetitively throughout your application. They should not be aware of where data comes from, only how to present it.
Container and presentational components are also quite often referred to as “parent” and “child” components respectively. For the rest of the article, I will be using the terms interchangeably.
Violating the container/presentational flow is what ultimately causes developers to create components that quickly become unusable.
Example
Let’s say you have a Movie
component that displays the title of the movie, the year the movie was released, and the MPAA rating (“R”, “PG-13”, “PG”, etc…) for the latest upcoming release.
1
2
3
4
5
6
7
<template>
<div class="movie">
<div class="movie-title"></div>
<div class="movie-rating"></div>
<div class="movie-year"></div>
</div>
</template>
So far, so good. We have declared in our <template>
tag the structure of how we want to render the featured movie. The next step is to actually initialize the data so we can display the featured movie of the week on our site.
Assume that we need to retrieve the data from some API. That API could be IMDb, Netflix, our own API - those details don’t matter to us. What matters is how we handle loading that data into the component on page load.
Now, if you were like me, when I first started using Vue, I found myself doing something much like this:
Movie.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<template>
<div class="movie">
<div class="movie-title"></div>
<div class="movie-rating"></div>
<div class="movie-year"></div>
</div>
</template>
<script>
import axios from 'axios'
export default {
data () {
return {
title: '',
rating: '',
year: ''
}
},
/**
* Making an AJAX call to initialize the data when the
* page loads and setting the response to data properties.
*/
mounted () {
axios.get('/some/api/movies/1')
.then(res => {
this.title = res.data.title
this.rating = res.data.rating
this.year = res.data.year
})
}
}
</script>
The Problems This Causes
The first problem is, since the API call to axios
is being called on mounted
, and mounted
is called every time you “mount” the component to the DOM (basically, every time you create a Movie
component), that means every time you create a Movie
component, you also create an AJAX call. That might be okay in certain cases; perhaps only one featured movie is ever displayed throughout your site and you’ll never display more than one at a time. However, what happens when you need to show two featured movies? Or 10? Or 100? If you try to display 100 movies with a component that looks like the one above, you’ll end up making 100 asynchronous HTTP requests. That is 99 HTTP requests more than needed to fetch the same information.
The second problem is that the data within the component is fixed, including the API endpoint /some/api/movies/1
. So, not only would you generate 100 featured Movie
components, all 100 Movie
components would be loading the exact same movie.
I’ve seen some pretty funky attempts from new Vue developers at trying to solve this issue by doing things such as passing in the movie’s id as a prop. Kind of like this:
Movie.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
<div class="movie">
<div class="movie-title"></div>
<div class="movie-rating"></div>
<div class="movie-year"></div>
</div>
</template>
<script>
import axios from 'axios'
export default {
/**
* DON'T DO THIS
*
* Added movieId prop so I can pass in the id of a movie
* from the API and load it.
*/
props: {
movieId: {
type: String
}
},
data () {
return {
title: '',
rating: '',
year: ''
}
},
mounted () {
axios.get(`/some/api/movies/${movieId}`) // looks clean, but behaves badly
.then(res => {
this.title = res.data.title
this.rating = res.data.rating
this.year = res.data.year
})
}
}
</script>
In the example above, we are adding a props
property to the Movie
component so that we can pass in the id of the movie as movieId
and then load the data for a specific movie into the Movie
component.
However, there are three new issues here.
The first issue is that this still doesn’t solve the first problem, which is that we are still making an HTTP request every time a component is created and/or mounted to the DOM.
The second issue is that you can never really load default values. Maybe there’s no featured movie available and you want to display a default title instead: “Movie Unavailable”. Sure, you could preset some “default” values for the data
properties, but you still end up making an HTTP request when you load the component, and if the request fails, you end up generating unnecessary errors in your console.
The third issue is that loading your data this way does not allow you very much flexibility on your ability to easily retrieve movies from multiple different sources. In our example, we’re retrieving movies from /some/api/movies/1
, but what if I also want to display movies from a different API, for example /different/api/movies/123
? In this case you would also have to pass in the URL as a prop (yuck), forcing this poor presentational component to know how to handle different API requests and responses. Presentational components just want to be dumb and simple. Let them be dumb and simple.
Solution
There’s two viable options here:
-
If the state management in your application is simple, then creating a simple parent Vue component should suffice for managing application state and the retrieval of data.
-
If the state management in your application is complicated (maybe you’re running a single page application with a lot of moving parts), then you should look into a “flux” library such as Vuex as your solution instead.
I am going to assume you’re working with a simple application. Either way, the principles are still the same, just executed a little differently. Vuex is a topic amongst itself which I’ll cover in another article, but don’t worry, if you do not know if you need Vuex, then you don’t need Vuex. Keep your life as simple as possible and don’t prematurely optimize until you absolutely have to. Dan Abramov, the author of the most popular flux library called Redux, has a great quote about this:
Flux libraries are like glasses: you’ll know when you need them.
Now, assuming you have an application with simple state management, then a proper solution would be to load the data from a parent component (which renders the Movie
component(s)) and pass the response data to the presentational component via props.
In the next example, I created a FeaturedMoviesPage.vue
component which would represent a normal webpage in our movies application and serves as our parent component. This component, would fetch the data from the API(s) and then render the presentational Movie
component(s) while passing in the API response data in a loop.
Here’s an example:
FeaturedMoviesPage.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<movie
v-for="movie in movies"
:key="movie.id"
:title="movie.title"
:rating="movie.rating"
:year="movie.year"
/>
</template>
<script>
import axios from 'axios'
import Movie from './Movie.vue'
export default {
data () {
return {
movies: []
}
},
mounted () {
axios.get(`/some/api/movies`) // getting multiple movies
.then(res => {
this.movies = res.data.movies
})
}
}
</script>
Movie.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div class="movie">
<div class="movie-title"></div>
<div class="movie-rating"></div>
<div class="movie-year"></div>
</div>
</template>
<script>
import axios from 'axios'
export default {
/**
* No more `data` and no more `mounted`, just dummy values! Yay!
*/
props: {
title: {
type: String,
default: 'Movie Unavailable'
},
rating: {
type: String,
default: 'N/A',
validator: (value) => {
return ['G', 'PG', 'PG-13', 'R'].includes(value)
}
},
year: {
type: String,
default: 'N/A'
}
}
}
</script>
So what’s so great about this solution? Well, for one, this Movie
component no longer issues any HTTP requests. This means our application is only performing one HTTP request to retrieve all the featured movies from whichever API it needs to.
Secondly, if a movie may happen to not exist, our default values that we have set in props
will kick in and at least show something to the user without generating any errors.
Thirdly, we can take full advantage of validating the type and value of each data point (title
, rating
, and year
) with props
. We can ensure that our values must be strings, and we can even validate that the rating
is an acceptable value. Now imagine trying to do the same thing but using data
instead of props
. As you could imagine, that would produce a lot more code than necessary. You would most likely have to perform all your validations in lifecycle hooks (mounted
, created
, etc…) or in the callbacks of your HTTP responses.
Lastly, the Movie
component no longer cares about where data is coming from; it only cares that it accepts the right prop value and that’s it. This means the Movie
component can be rendering data from Netflix, IMDb, or somewhere else. It doesn’t care. Basically, that is for you parent component to determine and to mold the data into a way that Movie
component can understand it, and that way, is through it’s props.
Conclusion
Of course, there are some cases where using data might make more sense. In most cases, I find myself using data
instead of props when dealing with forms. But this is a special exception. A rule of thumb I like to go by for when to use props
or when to use data
instead: if the data is coming from you (the user), use data
; if the data is coming from somewhere else (e.g. an API), use props
.
Essentially, the easiest way to follow the parent/child principle is simply to favor using props
instead of data
as much as possible to present your data. Make it your “go-to” way of presenting data in a component. That will force you to pass the data from somewhere outside the component and that will do a great job of keeping most of your components reusable. It will even make unit testing your presentational components A LOT simpler.
If you’re not sure if you should use props
or data
, try solving your problem with props
first. It offers a lot of convenience, which is what it is intended for.