1
\$\begingroup\$

I have made a small Vue app which fulfills the following user stories:

Given I make a search for an existing GitHub user, then:

  • The user's username is shown
  • The user's avatar is shown The user's most commonly used language, if available, in their repositories is shown as a 'favourite language'
  • The user's followers, if there are any, are shown alongside their amount

Here is a working demo.

Here is my single file component:

<template>
  <div id="app">
    <form @submit.prevent="submit">
      <input v-model="inputValue" type="text" placeholder="Enter a GitHub username...">
      <button>Go!</button>
    </form>
    <h1 v-text="username"></h1>
    <img v-if="avatar" :src="avatar" class="avatar">
    <h2 v-if="faveLang">😍 Favourite Language: {{faveLang}} 😍</h2>
    <h3 v-if="followers.length > 0">Followers ({{followers.length}}):</h3>
    <ul v-if="followers.length > 0">
      <li v-for="follower in followers">
        {{follower}}
      </li>
    </ul>
  </div>
</template>

<script>
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
import _ from 'lodash'

Vue.use(VueAxios, axios)

export default {
  name: 'app',

  data() {
    return {
      inputValue: '',
      username: '',
      avatar: '',
      followers: [],
      faveLang: '',
      urlBase: 'https://api.github.com/users',
    }
  },

  methods: {
    submit() {
      if (!!this.inputValue) {
        const api = `${this.urlBase}/${this.inputValue}`

        this.fetchUser(api)
      }
    },

    fetchUser(api) {
      Vue.axios.get(api).then((response) => {
        const { data } = response

        this.inputValue = ''
        this.username = data.login
        this.avatar = data.avatar_url

        this.fetchFollowers()
        this.fetchFaveLang()
      }).catch(error => {
        console.warn('ERROR:', error)
      })
    },

    fetchFollowers() {
      Vue.axios.get(`${this.urlBase}/${this.username}/followers`).then(followersResponse => {
        this.followers = followersResponse.data.map(follower => {
          return follower.login
        })
      })
    },

    fetchFaveLang() {
      Vue.axios.get(`${this.urlBase}/${this.username}/repos`).then(reposResponse => {
        const langs = reposResponse.data.map(repo => {
          return repo.language
        })

        // Get most commonly occurring string from array
        const faveLang = _.chain(langs).countBy().toPairs().maxBy(_.last).head().value()
        if (faveLang !== 'null') {
          this.faveLang = faveLang
        }
      })
    }
  }
}
</script>

<style>
body {
    background-color: #daa520;
}

#app {
    display: flex;
    align-items: center;
    flex-flow: column;
    font-family: Comic Sans MS;
}

input {
    width: 320px;
}

input,
button {
    font-size: 25px;
}

h1 {
    font-size: 44px;
}

.avatar {
    height: 200px;
    width: 200px;
    border-radius: 10%;
}
</style>

My questions:

  1. Is this a good way of managing changing values, ie by mutating the data object? Or should this be represented in state?

  2. How can I better structure this app? It could be split up into components but should it?

  3. Is there a better way to make API calls like this, and should they go in the methods section?

  4. Should the API calls be placed inside the mounted method?

\$\endgroup\$
2
\$\begingroup\$
  1. This works. State works as well but you don't need it until your app gets more complex.
  2. There are different ways to structure this app, but it doesn't necessarily make them better. I was able to grok this in just a few minutes, which is great. I was able to read it from top to bottom without jumping around the file, which is great. Your style is largely consistent throughout the app, which is great.

Points 3 and 4 are mostly the same as above. You can structure it differently, but it's not necessary for a small app like this. As the app grows you will notice some areas that can easily be refactored into their own pieces, and I think you've mentioned most of them.

Here are some ideas you can use as a learning exercise. Understanding them now will help you reason about larger apps in the future. They're not necessarily the correct way, just learning exercises:

  1. Try using a component for the form and another for the stats. Think about how you would communicate the data between these two components.
  2. Write unit tests to prove that your component(s) are working as expected. Currently you have a lot of logic in your component methods. Is it easy to test this logic with unit tests? Would it be easier to test this logic if it were separated from the controller?
  3. Try extracting the API logic into another class that you can import. Currently your controller has data and methods that can be used by your template, but they are not. Think of these as 'public' properties and methods. Do they need to be public? Why would you not want to make them public?
  4. Currently your methods are fetching data and then mutating the results. Try storing the raw data, and then use computed properties to alter the data for display. Does this approach make it easier or harder to reason about the logic? How does it affect the testability of your component?
\$\endgroup\$
0
\$\begingroup\$

Re to @havan59er:

  1. I split the large component into UserForm and Stats components. This helped me understand how values are passed between components:

UserForm.vue Stats.vue

  1. This was the one I struggled most with. I could successfully run unit tests but not with anything to do with async, despite what I interpret to be following the documentation. I outlined my issues in this StackOverflow post.

  2. I created a FetchMethods.js file containing methods that are now used by App.vue. I abstracted some logic away from App, but now my code is a little long and makes use of promises. I hope I got the right idea with this? Separately, I created a userData property on my data object in App.vue so that the response object's properties are available anywhere in App.vue.

  3. I wasn't able to unit test my async stuff but I think I abstracted some logic away from the App methods so this became clearer.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.