Patterns

In this chapter, we'll look at a variety of anti-patterns within Vue.js and review concepts at a high level that we've learned throughout the book. We'll look at various patterns and anti-patterns and how we can write code that is consistent across teams and your own projects.

Before defining anything as apattern_or_anti-pattern, it's important to accurately define both for the reader. If something is to be considered a pattern, this means that this is a recommended practice in the vast majority of cases. On the contrary, if I've defined it as an anti-pattern, then it's most likely not a recommended practice in a vast majority of cases. For further information on this, a good source of patterns and guidelines can be found athttps://github.com/pablohpsilva/vuejs-component-style-guide.

Software development is an opinionated field with a variety of ways to solve the same problem, so there may be ideologies that are classified as something you don't agree with, and that's okay. At the end of the day, each team has their own style, but developers should seek to stick to patterns that reduce friction and speed up development where possible.

In this chapter, we'll learn about the following topics:

  • Common patterns and anti-patterns within Vue projects
  • Container/presentational components
  • How to write testable Vue.js components

Components

There are many ways for components to communicate within Vue, such as the use of props, events, and store-based scenarios. Vue also gives us access to$parentand$childrenobjects, which allow us to interact with parent/child components. Let's take a look at this and see why it shouldn't be used.

Communication – Anti-pattern

Imagine that we had our familiarTodoListexample, and within theTodoItemcomponent, we want the ability to complete a particular Todo. If we wanted to keep our todos within theTodoListand thus call the completed method fromTodoItem, we could do it like this:

export default {


  props: ['todo'],


  methods: {


    completeTodo(todo) {


      this.$parent.$options.methods.completeTodo(todo);


    }


  }


}

This is a bad idea for numerous reasons, mostly because we're tightly coupling these two components together and assuming that there will always be acompleteTodomethod on the parent component.

What can we change about this to make it better?

I'm not saying that parent/child components can't communicate, but you should aim to design components in such a way that they're flexible. Use events or the Vuex store depending on the structure of your application. Here's an example using an event instead:

methods: {


  completeTodo(todo) {


    this.$emit('completeTodo', todo)


  }


}

Children mutating props – Anti-pattern

It's important that we won't modify props inside our child components. Props should be considered the source of truth when passed down to a component and thus, change the value from within a child component is typically bad practice. There are some unique case scenarios however where it may be appropriate to do so, like when using the.syncmodifier to achieve two-way data binding.

If we use our previous example and change the todos prop from within the child, we'll get a warning inside the console:

methods: {


  completeTodo(todo) {


    this.todo = [{id: 1, name: 'Do the dishes.'}];


    this.$emit('completeTodo', todo)


  }


}

What should we do instead?

If you want to work with the prop inside the child component, it's best to save the prop as a new variable inside thedataoption. This allows you to then mutate a new version of the prop, local to this component:

export default {


  props: {


    age: {


      type: Number,


    }


  },


  data() {


    return {


      personAge: this.age


    }


  },


}

We can then access and mutatepersonAgewithout worrying about any side effects. Another example can be seen when creating a filterable search box, where instead of mutating the prop directly, make acomputedproperty that performs the required functions:

export default {


  props: {


    filter: {


      type: String,


    }


  },


  computed: {


    trimFilter() {


      return this.filter.trim().toLowerCase()


    }


  }


}

Mutating property arrays

An important consideration to make when passing down arrays and objects as properties within JavaScript is the fact that they are passed by reference. This means that any changes to the original array within the child will also spill over into the parent component. Let's see this in action:

<template>
  <div>
    <h1>Parent Component</h1>
    <ul>
      <li v-for="friend in friendList" :key="friend.id">{{friend.name}}</li>
    </ul>

    <Person :friendList="friendList" />
  </div>
</template>

<script>
import Person from './components/Person';

export default {
  data() {
    return {
      friendList: [{ id: 1, name: 'John Doe' }]
    }
  },
  components: {
    Person
  }
}
</script>

Here, we have a component (App.vue) that contains an array that we're displaying on screen and passing down into the child component. We'll display the same array on screen inside the child component, but also give the child a button to add a new item into the array:

<template>
  <div>
    <h1>Child Component</h1>
    <ul>
      <li v-for="friend in friendList" :key="friend.id">{{friend.name}}</li>
    </ul>
    <button @click="addFriend">Add Friend</button>
  </div> 
</template>

<script>
export default {
  props: {
    friendList: {
      type: Array,
    }
  },
  methods: {
    addFriend() {
      this.friendList.push({ id: 2, name: 'Sarah Doe' })
    }
  }
}
</script>

When we add a new person to our friends' list, this is our result:

Then, both components have the same array! This isn't what we want. If for some reason we wanted to do an action like this, it would be wiser to keep a copy of the friends' list and mutate that, as follows:

export default {
  props: {
    friendList: {
      type: Array,
    }
  },
  data() {
    return {
      fList: [...this.friendList]
    }
  },
  methods: {
    addFriend() {
      this.fList.push({ id: 2, name: 'Sarah Doe' })
    }
  }
}

Using data as an object - Anti-Pattern

When creating components with Vue, it's important that the data option is a function that returns a new object holding data, rather than just a plain data object.

If you simply use a data object that's not a function, all instances of the component will share the same data. This is bad because as you may be able to imagine, all instances of the component will be updated with the same data any time it changes. It's important to ensure that each component is capable of managing its own data rather than sharing data across the board.

Let's take a look at the problem:

data: {


 recipeList: [],


 selectedCategory: 'Desserts'


}

We can fix this by doing this instead:

data () {


 return {


  recipeList: [],


  selectedCategory: 'Desserts'


 }


}

By creating thereturnstatement, it allows each instance created to have its own object rather than a shared one. This then allows the code to be used multiple times without the conflict of shared data.

Next up, let's take a look at best practices for naming our components.

Naming components – Anti-pattern

It's not a good idea to name components in single words as it has the chance to conflict with native HTML elements. Let's say we had a signup form and a component namedForm.vue; what would be an appropriate name when using this inside our template?

Well, as you might imagine, having a component named<form>will conflict with the<form>, so it's a best practice to have components that are named with more than one word. A better example can be the name ofsignup-form,app-signup-form, orapp-form, depending on preference:

// This would not be an appropriate name as it conflicts with HTML elements.


Vue.component('form', Form)




// This is a better name as it's multi-word and there are less chances to conflict.


Vue.component('signup-form', Form)

Template expressions

Often times, when we're displaying items on the screen, we may have to compute values and call functions to change the way our data looks. Instead of doing this work inside the template, it's advised to move this out into acomputedproperty, as this is much easier to maintain:

// Bad 
<nuxt-link :to="`/categories/${this.category.id}`" class="card-footer-item">View</nuxt-link>

// Good
<nuxt-link :to="categoryLink" class="card-footer-item">View</nuxt-link>

export default {
  props: ['category'],
  computed: {
    categoryLink () {
      return `/categories/${this.category.id}`
    }
  }
}

This means any changes in our template are much easier to handle because the output is mapped to a computed property.

Pattern – Container/Presentational components

An important part of the component design is ensuring that your components are testable. You should think of each component as a standalone module in your application that could be switched in/out, as necessary; as a result, it should not be tightly coupled with another component.

The best way is to ensure that your components are testable after ensuring that light coupling is to have a well-defined public API via component props and then use events to communicate between the parent/child component. This also helps us when testing, as we're able to mock components much easier.

A common pattern to use when following this model is the container/presentational components. This means we keep all of our business logic and state inside the "container" and then pass the state down to the "presentational" component to display on the screen.

The presentational component can still communicate with other components, if necessary, with the use of events, but it shouldn't modify or hold state outside inbound props.This ensures that we have a common data flow between our components, and it means that our applications are easier to reason about.

Here's an explicitly named component—DogContainer.vue:

<template>
  <dog-presentational :dogName="dogName" @woof="woof"></dog-presentational>
</template>

<script>
import DogPresentational from './DogPresentational'

export default {
  data() {
    return {
      dogName: 'Coco',
    }
  },
  components: {
    'dog-presentational': DogPresentational
  },
  methods: {
    woof() {
      alert('Woof!');
    }
  },
}
</script>

The container component has handed down the dog's name (and any other items) as a property into the presentational component. The container component is also listening for an event namedwoofon this component and is taking action by calling thewoofmethod when it has been emitted. Here's our presentational component:

<template>
  <div>
    <h1>Name: {{dogName}}</h1>
    <button @click="woof">Woof</button>
  </div>
</template>

<script>
export default {
  props: ['dogName'],
  methods: {
    woof() {
      this.$emit('woof')
    }
  }
}
</script>

Our component's concerns are now clearly separated, and we have a clear communication path between them.

This composition can be visualized in the following figure:

Composing components

Prop validation

While we should seek to communicate between child components via props, it's important to be verbose when validating properties by considering types, requirements, defaults, and so on. Throughout the book, I've used a mix of both techniques for brevity, but in production, props should be appropriately validated. Let's start out by looking at some examples of property types:

export default {
  props: {
    firstName: {
      type: String
    },
    lastName: {
      type: String
    },
    age: {
      type: Number
    },
    friendList: {
      type: Array
    }
  },
}

We also have various other types available such as Boolean, function, or any other constructor function (that is, type of Person). By accurately defining the types we expect, this allows us (and our team) to reason better about what we can expect within this component.

At the same time, we can also ensure that props are required. This should be done where necessary, allowing us to ensure that whenever the component is used, no required props are missing:

  props: {
    firstName: {
      type: String,
      required: true,
      default: 'John'
    },
    lastName: {
      type: String,
      required: true,
      default: 'Doe'
    }
  }

We should always seek to give our props default values where possible, as this reduces necessary configurations but still allows the component to be customized if a developer wants. When dealing with objects and arrays, a function is used as a default parameter to avoid issues where instances share the same value.

props: {
  firstName: {
    type: String,
    default: 'John'
  },
  lastName: {
    type: String,
    default: 'Doe'
  },
  friendList: {
    type: Array,
    default: () => [{ id: 1, name: 'Paul Halliday'}]
  }
}

We can also assign a customvalidatorfunction for our properties. Let's say that we have a slotmachinecomponent that is only rendered if the user is18years or older:

   props: {
    age: {
      type: Number,
      required: true,
      validator: value => {
        return value >= 18
      }
    },
  }

Understanding reactivity

We've discussedreactivity and how it can be usedin the previous chapters, but it's important to reconsider. When we're creating reactive data objects within Vue, it takes each property and adds appropriate getters/setters usingObject.defineProperty. This allows Vue to handle changes to the object and notifies watchers, subsequently updating the componenthttps://vuejs.org/v2/guide/reactivity.html. This can be visualized like so:

Visualizing Reactivity

This means that any property defined in the data option is automatically reactive. Here's an example:

export default {


  data() {


    return {


      name: 'Vue.js'


    }


  }


}

Thenameproperty is reactive inside this Vue instance, but if we were to add another property outside of the Vue instance, this would not be reactive. Let's take a look at an example:

export default {


  data() {


    return {


      items: [


        { id: 1, name: 'Bananas'},


        { id: 2, name: 'Pizza', quantity: 2},


        { id: 3, name: 'Cheesecake', quantity: 5}


      ] 


    }


  },


}

Ourgroceriescomponent has an items array that contains various objects. Every object has a quantity apart from theBananasobject, but we'd like to set the quantity for this. When usingv-forit's important to includev-bind:key(or the shorthand:key) as it acts as a unique identifier for each item and by doing so allows for reuse and reordering of each node. Whilstkeymay be an attribute forv-forkeep in mind it does have other use case scenarios.

<template>
  <div>
    <ul>
      <li v-for="(item, index) in items" :key="item.id" @click="addQuantity(index)">
        {{item.name}} {{item.quantity}}
      </li>
    </ul>
  </div> 
</template>

<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: 'Bananas'},
        { id: 2, name: 'Pizza', quantity: 2},
        { id: 3, name: 'Cheesecake', quantity: 5}
      ] 
    }
  },
  methods: {
    addQuantity(selected) {
      this.items[selected].quantity = 1;
      console.log(this.items);
    }
  }
}

If we then head over to our browser, and proceed to use the dev tools to access theconsolewe can see that the quantity has been set to hold a value for our object.

Note how there are reactive getters and setters for the quantity objects that contain quantity when defined inside the data object. Adding a property to the items after the fact means Vue doesn't add the appropriate getters/setters. If we wanted to do this, we'd have to useVue.setinstead:

methods: {


  addQuantity(selected) {


    const selectedItem = this.items[selected];


    this.$set(selectedItem, 'quantity', 2)


    console.log(this.items);


  }


}

This time, there are getters/setters for every property inside our instance:

Summary

In this chapter we looked at anti-patterns and patterns, and we have expanded our knowledge as to not only what they are, but also when it is appropriate to use them to coincide with best practices. Not only that, we also reviewed a lot of the concepts that we learned throughout the book in this chapter, along with considering some new ideas and techniques of what can be used going forward.

Reflecting on the previous chapters, we can look back and see how much ground we've covered. Practicing the techniques covered in this book will allow you to create scalable applications with Vue.js and build on what you've learned. Another important thing to remember is thatweb development is always evolving, the amount ofpractical applications_for Vue continues to grow and_so should you.

What next? Try new things! Build new projects, attend Vue.js meetings and conferences - find new ways of applying your skills to teach others. Not only will you have a positive impact on others, but you'll reaffirm your skills as a developer.

results matching ""

    No results matching ""