We've now seen how to define a Vue component, as well as how to declare data properties and render them into the DOM using component templates. However, this is just scratching the surface of what we can do with Vue components. We've already discussed how Vue components have two main aspects to them: presentation and behavior. Let's start to look at what else we can do with the behavior side of a component, starting by expanding on the data function and talking about state.
Component behavior
State
When talking about the state of an application, what do we actually mean? As soon as we introduce complex client-side logic to a web application, we also introduce multiple meanings of the word state, or rather, we introduce an additional type of state to our application. State can mean different things depending on the type of state we are interested in.
Most backend .NET developers will probably understand state to be based on a snapshot of the applications database at any point in time. In terms of our e-commerce example from earlier, this would include the current list of products and categories that make up our catalog; a list of users or customers who have registered for an account; and a list of orders and associated order items. This form of state is based on the domain of the application, and can be extended to include things that don't necessarily persist into the database, such as authentication, validation, and business rules that control how the application behaves.
The type of state that we care about at the component level is known as UI state. Generally speaking, UI state and domain state are separate things, but it isn't impossible for the two to cross over. For example, keeping track of the current user isn't necessarily a UI concern, but most SPAs will use some form of JWT authentication where the user's tokens, and as such their authentication state, will be tracked by the SPA. Another example is where we display paginated lists of data in an SPA—keeping track of the currently displayed list items is a UI concern, but we are still displaying a subset of the database-persisted items that belong to the domain of the application.
Other examples of UI state include keeping track of the active menu item in a navigation component; controlling the visibility of a modal window or custom drop-down menu; keeping track of which panels are open/closed in an accordion; and showing and hiding loading spinners during AJAX operations. These are fairly simple examples, and there are a lot more complex things that we can do with client-side state such as transitions and animations, but it's enough to demonstrate what we are talking about for now.
Each of our components are only responsible for their own subset of the application UI state. For example, a component that contains the filters that we've applied to a product list is only concerned with the selected values of those filter controls. It isn't—and shouldn't be—concerned with which user is currently logged in, or how many items the user has added to their shopping cart. This is all part of adhering to the single responsibility principle, which makes our components much easier to debug and maintain.
Sooner or later, we're going to come across a situation where a single component is in violation of the SRP, and we want to break that component down into a parent-child relationship instead. A common pattern is where we have a list component as a parent that contains a collection of list-item components as its children. The original component was probably already fetching the data it displays, and it makes sense to leave that responsibility up to the new parent list component; after all, the only alternative is to have each list-item component fetch its own data, which would result in multiple trips to the server instead of just one.
Props
We already know that components are self-contained, so how do the children get access to the data they need to display if the parent owns and controls it? The simple answer to that question is props. Props are a means of parent components passing data down into their children. The child component must explicitly declare the names of the props it expects to receive, and then these props can be referenced in much the same way that we do for any other piece of data that the component owns.
The following code demonstrates how we declare and reference a prop within a child component:
<template>
<div class="product">
{{ name }}
</div>
</template>
<script>
export default {
name: 'product',
props: ['name']
}
</script>
We can render this child component from within its parent component template as follows:
<template>
<div class="product">
<child-component name="Hands on Vue.js and ASP.NET Core" />
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
name: 'parent',
components: {
ChildComponent
}
}
</script>
At this stage, it is important to understand that this method of sharing data between components is strictly limited to one-way. It is impossible to send data back up the chain from a child to a parent using props. We'll look at how to communicate in the opposite direction later in this chapter.
The final point to mention about props is that Vue provides a means of validating the props being passed to a component. We can perform basic type checking; control whether props are required or optional; configure default values in the event that a prop is not provided; and even write custom validator functions in much the same way as we would with client-side validation libraries. The following code snippet shows an example of some of these validation rules and how we describe them in the component definition:
<script>
export default {
name: 'validation',
props: {
name: {
type: String,
required: true
},
description: {
type: String,
required: true
},
price: {
type: Number,
required: true
}
}
}
</script>
Methods
If the data we are displaying within our components never changes, it's probably a sign that we really don't need to be using an SPA framework such as Vue. We know that the data in a component is used for things such as showing and hiding modal windows, so how do we actually change the data so that the UI can become reactive? Vue components can declare methods in order to manipulate their data. These methods are standard JavaScript functions, and automatically have their function context (that is, the value of this) bound to the component instance so that they can access its data, props, and computed properties. The following code shows how we can increment a simple counter using a method on a Vue component. We can trigger this method by calling it from a UI element event handler, which we'll look at later in this chapter:
<script>
export default {
name: 'methods',
data () {
return {
counter: 1
}
},
methods: {
increment () {
this.counter++
}
}
}
</script>
Computed properties
As our applications grow in complexity, the chances are that sooner or later we'll need to perform some logic on one or more of our component data items and display it in a template. As a simple example, we may have data properties for a person's first and last name, but we are regularly required to concatenate them and display their full name. We could just use expressions inside the template as follows:
<template>
<div>
{{ firstName + ' ' + lastName }}
</div>
</template>
<script>
export default {
name: 'expressions',
data () {
return {
firstName: 'Stu',
lastName: 'Ratcliffe'
}
}
}
</script>
However, if we've duplicated this expression in multiple places throughout the template, and we then decide to change the way we display the person's full name, we have multiple places to find and update it. Even in a fairly small and simple example such as this one, this doesn't sound like much fun, and certainly isn't very maintainable.
Alternatively; we could use a computed property to achieve the same result. If you've ever used a computed column in SQL Server, then the concept will be familiar to you, and computed properties in Vue components behave in much the same way. In the following code snippet, we can see how we would declare a fullName computed property in our component declaration, and how we then render the value of that property into a template:
<template>
<div>
{{ fullName }}
</div>
</template>
<script>
export default {
name: 'computed-properties',
data () {
return {
firstName: 'Stu',
lastName: 'Ratcliffe'
}
},
computed: {
fullName () {
return `${this.firstName} ${this.lastName}`
}
}
}
</script>
Although they are referred to and referenced in the same way as standard data properties, they are actually functions. As with the component data object and methods, if Vue detects that the returned value of a computed property has changed, it will automatically refresh the UI to reflect those changes.
Watchers
Watch properties are similar in functionality to computed properties, and in most cases it is actually unnecessary to use one. However, there is a limitation in that computed properties are synchronous functions and always return a value that we can bind to. This makes them impossible to use alongside asynchronous operations such as AJAX calls. So, what should we do if we want to trigger an AJAX call to our API server and react to the data that is returned?
Say we wanted to automatically display search results as a user enters their search term into a text input. We could implement a simple event listener on the input and create a method that is triggered on the keyup event, which performs an AJAX request with its current value. This would work absolutely fine, and you may never need any more control than this. However, this piece of functionality is completely coupled with the text input, and as such our component will not react and refresh the UI if we change the data value of the input directly.
Watch properties are a solution to this problem, as they provide a far more generic way of reacting to data changes. In the following example, we are performing an AJAX request that queries an API to perform some sort of search. We'll discuss what v-model, v-show, and v-for mean shortly, but for now, just know that the text input has its value bound to the searchValue property, and we display each search result in the array as a list item element:
<template>
<div>
<h1>Watchers Example</h1>
<input v-model="searchValue" />
<span v-show="loading">Loading...</span>
<ul>
<li v-for="result in searchResults">{{ result }}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'watchers',
data () {
return {
searchValue: '',
searchResults: [],
loading: false
}
},
watch: {
searchValue (newValue) {
let vm = this
this.loading = true
setTimeout(function () {
vm.searchResults.push('some', 'search', 'results')
vm.loading = false
}, 500)
}
}
}
</script>
Note how we also have to declare a data property of the same name. The watch declaration instructs the component to quite literally watch the data property, and run the associated function each time it changes. The current value of the data property is passed to the function so that we can use it in any way that we please.
We also made use of this function to update a loading property to instruct the component that the AJAX call is in progress; this property can now be used to show a loading spinner in the UI each time the AJAX call is triggered. We could even extend this function to limit how often the AJAX call can take place by using a debounce or throttle function. None of this is possible with a computed property, and by using a watch property, it doesn't matter whether we update the data through a UI control or manually in another component method!
Lifecycle hooks
Every Vue component goes through a number of steps to initialize it when it's created. Among other things, these steps are responsible for compiling and rendering the HTML template, setting up the component's data so it becomes reactive, and mounting the component into the DOM. In order to hook into this process, we're given a number of lifecycle hooks that we can use to run our own application-specific initialization code at each stage.
These lifecycle hooks are as follows, and named appropriately enough to make themselves fairly self-explanatory:
- beforeCreate
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeDestroy
- destroyed
beforeUpdate and updated run in a continuous loop for the duration of the component's lifetime. Every time Vue detects a data change within the component, these steps are run before and after the virtual DOM is re-rendered.
So, how do we actually make use of these function hooks? The following code shows an example of how we declare the hooks we wish to use within a component definition:
<script>
export default {
name: 'lifecycle-hooks',
beforeCreate () {
console.log('before create')
},
created () {
console.log('created')
},
beforeMount () {
console.log('before mount')
},
mounted () {
console.log('mounted')
},
beforeUpdate () {
console.log('before update')
},
updated () {
console.log('updated')
},
beforeDestroy () {
console.log('before destroy')
},
destroyed () {
console.log('destroyed')
}
}
</script>
We simply add a root-level function with its key matching the name of the lifecycle hook we want to use. This is all very well and good, but if you've never used an SPA framework such as Vue before, you're probably wondering why we'd ever want to bother doing this. There are many reasons to use lifecycle hooks, the most common of which is probably to fetch the data the component needs from an API somewhere. A good place to do this is in the created hook because we don't need access to the DOM, so there is no need to wait until the mounted hook is run later. The other reason to use created instead of mounted is because created is the only hook that is run on both client- and server-rendered versions of the component. We'll look at server-side rendering in one of the final chapters in this book!
A few other examples include triggering animations as soon as the page is displayed, and dirty checking a form to give the user a chance to complete it before navigating away. We would need to use the mounted and beforeDestroy hooks for these actions, respectively.
So far, we've been focusing on the Vue instances and the different ways they help us manage the data that our components are responsible for, but this is only half of the story. We're yet to see how Vue can help us actually display that data. Let's start focusing on the template section of our components!