Simple Photo App with Vue.js, Axios and Flickr API — Part 2

Refactoring Practice

Nathan Magyar
10 min readApr 4, 2019

Part 1 | Part 2

Welcome back! Previously we laid the groundwork for a simple Vue-based photo app that consumes the Flickr API via the Axios library. We successfully made our first request with the flickr.photos.search method and got images displaying nicely on the page.

Before we start adding too many new pages and API calls, I thought this would be a good moment to practice refactoring. In this segment, we’ll streamline how we go about calling Flickr methods. We’ll also improve the user experience of our app by adding a new SearchResults.vue page as well as a more visual loading effect for our image cards. Major thanks to Cameron Bothner for contributing many of these improvements!

Step 1: Refactor the Flickr API call

For any given API call we make in this project, there will be a fair amount of repetition. All of our GET calls will have the same method, url, api_key, format, etc.

To reduce this redundancy and keep our code DRY (don’t repeat yourself), let’s create a helper function called flickr in a separate file. This helper function will accept two arguments: method and params. method will be a string like 'photos.search' that we incorporate on line 13 below. params will be an object of any extra options we pass in that are needed by that particular method, such as the tag string. We use this in the flickr method below by spreading it into the params object (line 12).

Add a new file named flickr.js inside src with the following contents:

Now flickr.js is the one using axios and our api_key inside config.js. From now on we can import flickr.js's default function flickr() for any GET request we want to make!

Update Home.vue:

  1. Import flickr.js and removing import statements for axios and config. flickr.js now uses axios and config so we don’t need them here.
  2. Simplify the search() method. I previously had the API-related code spread between two functions: search() and fetchImages(). To make things more readable, let’s consolidate all the API stuff and data handling to be in fetchImages(). Empty out the search() method so it only does three things: update the loading status to true, call fetchImages(), then set loading back to false.
  3. Update fetchImages() to use the exported flickr method from flickr.js. Remember, it accepts two arguments, the rest of the method name we want to call (in this case we’re calling flickr.photos.search so we add 'photos.search'), and parameters that are more unique to that method (in this case, tags, extras, page, and per_page). Like the axios() method before, the flickr method returns a Promise that we resolve by assigning photo data to this.images. Now all the API/data handling is done in one place.

Step 2: Make new views and components

Right now we only have one page,Home.vue, where users search by keyword and see results. That actually sounds like a search results page, so let’s refactor what we’ve got into a few new pieces.

We’ll start by making a new route for a search results page, then we’ll create a NavBar.vue component that will be available on all pages, and we’ll finish by converting Home.vue into SearchResults.vue.

Step 2.1: Add a new route

Update router.js to include the following:

According to the above, our app will now have two pages/routes: Home and Search Results. We import Home and SearchResults from their respective .vuefiles in the views directory first. Then inside the routes array we specify the path we want to show them at (path), give them a name for easy reference in other parts of our app (name), and state which component we want to use for each route (component).

The route object for searchResults contains two extra things: the path string (line 18) has a dynamic tag property that represents whatever tag keyword a user is searching for. Then line 21 declares props as true, which means we’ll pass in the tag parameter to the SearchResults component as a prop by that name. That’s how our SearchResults component will know what tag keyword to search for.

Step 2.2: Create a NavBar component

We want users to be able to search from any page in our app, so the search/nav bar should be its own component that lives in the root of our project, App.vue. Make a file called NavBar.vue inside the components directory with the following:

The template is exactly the same as what we’ve been using in Home.vue. The difference lies in the search() method that gets executed. NavBar.vue will not handle the actual API request. That will be done in SearchResults.vue. So the only thing our NavBar component is responsible for is sending the user to the search results page and passing along the tag keyword they entered. That is what line 32 does:

this.$router.push({name: 'searchResults', params: {tag: this.tag}})

We update what route the user is at with $router's push method, which accepts an object with a few arguments: the name of the route we want to go to and any extra parameters that it is expecting. We want to go to the searchResults route so that’s the name we enter. That route also has a :tag parameter, so we pass this.tag to it. Once that is done we reset the NavBar's tag data property to an empty string, so the old search term doesn’t persist.

The last step here is to add NavBar.vue to App.vue, so it appears on every page:

  1. Import the component (line 9 above)
  2. Register it (line 13)
  3. Put it in the template (line 3)

Step 2.3: Convert Home.vue to SearchResults.vue

Start by renaming Home.vue to SearchResults.vue. Then create a new Home.vue file in views that looks like this:

SearchResults.vue needs a few more updates. In router.js we said that the SearchResults view would receive the tag as a prop, so add a props section with the tag name declared. Line 16 also specifies that it will be of type String. In the template, line 7 below adds an h1 tag to display the term the user searched for, since it no longer shows in the search input area.

You should now be able to enter a search term on the homepage and end up at the /search/:tag route with your search term displayed:

So we’re on the right component and our tag is getting passed in correctly… but we’re not seeing any results. That’s because we need to change the way we make our API call. Before we were doing it by clicking the “Go” button, but that serves a different purpose now (to take us to the search results page and pass along the search term).

We need to somehow call the search() method when the user gets to this page. The solution we’ll use for that is the created() lifecycle hook. We’ll use this hook because we need to make the API call and populate our data (like images) before the template is rendered. created() let’s us run this code right at the point we desire. And the reason we want to do all this before the template is rendered is because otherwise Vue will try to build the page with data that doesn’t exist yet, resulting in an error.

Just after the props section in SearchResults.vue, add the created() hook and a this.search() call:

<script>
export default {
...,
props: {
tag: String
},
created() {
this.search();
},
data() {...}
...
}
</script>

Now when you search from the homepage, you’ll get the images again!

But, if you try to perform a new search while you’re already on the search page, the search term and url seem to update, but the images don’t:

Now we need a way monitor when/if the tag prop in SearchResults changes. When it does change, we should perform a new API request. Thisis a job for watchers, which do exactly what we want to do. According to the Vue Documentation, “watchers are most useful when you want to perform asynchronous or expensive operations in response to changing data.” Bingo!

The syntax is as follows:

<script>
export default {
...,
created() {...},
watch: {
tag(value) {
this.search();
}
},
...
}
</script>

Inside the watch section we create a function with the name of the prop we want to watch. That function accepts an argument (whatever the new value is for that property). From there you could do other things with it, but we don’t need to because the new value is already set when the user comes to the search results page. All we need to do is call this.search() again.

Now users can search from the home page as well as the search results page!

Step 3: Refactor ImageCard component

Not everyone has great internet speed when using the web. To improve the experience for users with a slower connection, let’s add a loading state to our ImageCard component.

To do that we’ll need to add a loading property to ImageCard.vue that stores a Boolean value. This value will get passed in from the SearchResults view as a prop. When it’s true, it will apply some extra CSS classes to our CardImage html. When it’s false, those classes will not be added.

Step 3.1: Template updates

Update ImageCard.vue to have the following template:

Throughout the template we are applying the conditional class skeleton when loading is true:

:class="{ skeleton: loading }"

Inside the curly brackets, skeleton is the class string and loading is the prop we’re using to decide whether the class gets added or not.

Notice in the above template that we’re also simplifying the template strings for each piece of image data: {{ image.title }} is now {{ title }}. This means we’ll be making some computed properties in a minute.

Step 3.2: More detailed props

Next, update the props option of ImageCard to be:

props: { 
image: {
type: Object,
default() {
return {}
}
},
loading: {
type: Boolean,
default: false,
}

The above adds more detailed information about each prop. To add more information, props has to store an Object, not an Array. Inside the props object, each key is a prop’s name. That key can then store either just the type of the prop, such as loading: Boolean, or we can go even further, specifying a type and default value. While you don’t have to do any of this when you’re rapidly prototyping, it’s good practice in a real application to make this effort, as it can prevent things from breaking in the future.

Above we’re saying the image prop will be of type Object and return an empty object by default. loading will be a Boolean with false as its default.

Step 3.3: New computed properties

Now it’s time to add those computed properties I mentioned. Inside ImageCard.vue add a computed section with the following computed properties:

Most of these are pretty straight forward:

  • title() returns the image title if there is one, or a string of Untitled Image if there is no title
  • byline() returns a template literal string that includes the image’s ownername attribute
  • timestamp() uses the moment.js library we were already importing to format the date. No more filter. Just use the format() method directly.
  • viewCount() returns a formatted string for how many times the photo has been viewed. If this.image.views is 1, the string will be 1 view. Otherwise it will say, # views.

The slightly trickier one is imageUrl(). What is line 6 doing? It’s returning an external variable that doesn’t exist yet, TRANSPARENT GIF. Let’s add it now, just after the moment import statement, before export default {...}:

<script>
import moment form 'moment';
const TRANSPARENT_GIF = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'export default {...}
</script>

The value of TRANSPARENT_GIF is a base64 placeholder image that will appear while loading is true. We use this because the html img element has to have something as its src attribute, otherwise its height will collapse. An alternative approach would be to point to a transparent or placeholder PNG in our assets directory, but the above approach spares us a request and thus is faster. In short, the string is a shortcut for showing a transparent image.

imageUrl() will return this transparent GIF if loading is true. If loading is false, it will return the url of the image to be displayed.

Step 3.4: Skeleton styles

Add the necessary skeleton styles and glow animation to ImageCard.vue to make the skeleton cards appear:

Here is the entire ImageCard.vue file now:

Step 3.5: Modify SearchResults.vue

We’re almost there! We just need to update SearchResults.vue so it displays our snazzy new loading state. We do this by creating two different ul's: one for the loading cards, and one for the actual image cards. If loading is false in SearchResults.vue, we’ll display the actual image cards. Otherwise, we show an array of 6 imageCards with the loading prop set to true.

Line 10’s "n in 6" is just a shorter way to create a loop that will go around 6 times:

<image-card v-for="n in 6" :key="n" :loading="true" />

Lines 24–26 above also add one new computed property, isTagEmpty, which checks to see if this.tag is set to null or an empty string. We’ll insert this into our search() method to prevent users from submitting an empty search, which would cause a bad API request. Place an if statement to check isTagEmpty just inside the search method. If the tag isn’t empty, we’ll make the API request. More detailed error handling could be done here in the future, but this is sufficient for now:

search() {      
if (!this.isTagEmpty) {
this.loading = true;
this.fetchImages();
}
},

Recap & Next Steps

That’s it for this segment! It may not feel like we made anything “new”, but we’ve put ourselves in a great position. Our app is now more flexible and DRY, complete with:

  • a separate search results page
  • a NavBar component that’s usable on all pages
  • a fancy new loading state for image cards

Thanks again to Cameron Bothner for contributing many of these improvements, especially the ImageCard loading state effect!

Next time, in Part 3, we’ll add more functionality to the home page and start calling more methods from the Flickr API. ✌️

--

--

Nathan Magyar

User Experience Designer and Front End Developer, University of Michigan Office of Academic Innovation