Start Your First Vue Project with Components, Routing, and State Management

I want to state upfront that this article is meant for beginner/intermediate Vue users as I’ve written a separate article for the basics of Vue. In the beginner article, I explain the fundamental Vue properties, component composition, directives, a life cycle, methods. This article is demonstrates how to interact with components, routing, and state management.

Released in 2014, Vue.js is a lightweight front-end framework similar to React and Angular. React and Angular dominate the front-end framework market share. However, Vue is the fastest growing framework. Since Vue has a smaller ecosystem, Vue has fewer plugins and libraries. This also means there are fewer resolutions on stack exchange for Vue issues that you may encounter.

While Angular was created by Google and React was created by Facebook. Vue was created by Evan You (who previously worked with Angular at Google). He initially created Vue as a framework for prototyping. Since Vue is not tethered to a corporate sponsor, Vue is dedicated to supporting its users.

Vue’s core libraries and tools are supported by Vue such as Vuex (similar to Redux), Vue Router, Vue Devtools, and Vue CLI. On the other hand, React relies on 3rd party libraries such as Redux, React-Router-DOM, etc.

Each Vue component has three sections: template (HTML), script (Vue properties), and style (CSS). The logic in the scripts are written in JavaScript so I would strong suggest you brush up on your JavaScript skills.

Unlike React, which uses JSX for the view, Vue utilizes plain HTML which reduces the learning curve and makes it easier to port legacy apps made is HTML. Similar to other front-end frameworks, Vue implements a component-based approach to create single page applications (SPA). Like Angular, components are inserted into the view via an assigned HTML tag.

As we know, making changes to the DOM is time-consuming. Therefore, Vue creates a virtual DOM which mirrors the real DOM. Any time there is a change in the state, Vue tracks the difference between the new state and the old state. This difference is applied to the real DOM.

Vue implements two-way binding which is a connection between the data and the view. Whenever there are changes in the data, it will change the view. Additionally, changes in the view will cause changes in the data.

Getting Started: In your command line, navigate to the folder you want to create your Vue project.

Install the Vue cli: The command below names the folder of our project “vue-cli”

npm install -g vue-cli

Initialize a Vue project: Confirm the default settings by pressing enter for each configuration

vue init webpack-simple vue-cli

Install the node modules:

npm install

Start the development server on port: 8080

 npm run dev

Understanding the Vue structure: The index.html file is being served by the development server. Within the index.html file, there is a <div id=”app”>. In src/main.js, we create a new Vue instance with {el: ‘#app} with attaches the Vue instance to <div id=”app”>. We also import App.vue (our home page) and set {render: h=>h(App)} which will render the App as our template.

import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
render: h => h(App)
})

App.vue is a component. Each Vue component has three sections: template (HTML), script (Vue properties), and style (CSS). Keep in mind, the data property must be a function that returns an object. Make sure to wrap all of your template code in one <div>. The data stored inside of this function is our local state. Whenever the data changes, the component will rerender.

<template>
<div>
<h1>{{status}}</h1>
</div>
</template>
<script>
export default {
data: function() {
return {
status: "Critical"
};
},
};
</script>
<style lang="scss">
</style>

The following command will build your app and minify files

npm run build

Importing Components Locally: To use a component, import it. Then, pick a unique name (i.e. base-card) for that component. Set the component’s name to the imported name (BaseCard). Now, you can insert the imported component as an HTML tag <base-card> in your component.

<template>
<base-card>
</base-card>

</template>
<script>
import BaseCard from "../UI/BaseCard";
export default {
components: {
base-card: BaseCard
},

};
</script>

Importing Components Globally: You can import components globally so you don’t have to keep importing a given component into every component. To do so, import the component into the main.js file (where you initially instantiate your Vue instance). Prior to instantiating the Vue instance, call the component method. The first argument will be the component’s name (base-card) and the second argument is the imported component (BaseCard). Now, you can you the imported component anywhere in your app by using <base-card></base-card>

import Vue from "vue";
import App from "./App.vue";
import BaseCard from "./theLearningResource/components/UI/BaseCard.vue";
Vue.component("base-card", BaseCard);const app = new Vue({
el: "#app",
render: h => h(App)
});

Styling: By default, all styles declared inside of a component will be applied globally. To apply the styling locally, apply the “scoped” attribute to the <style> element. Therefore, if you inspect the element in the real DOM, there will be a unique ID to which the styling is applied.

<style scoped>
background-color: red;
</style>

Passing Static Props: You can pass a static prop into a child component. In the example below, we pass a “name ” prop into the UserDetail component. The “yourName” string will be passed into the UserDetail component. Once the prop in inserted into your component, it is an anti-patter to mutate the prop.

<template>
<app-user-detail name="yourName"></app-user-detail>
</template>
<script>
import UserDetail from "./UserDetail.vue";
export default {
components: {
appUserDetail: UserDetail,
},
};
</script>

Accessing Props in Child Component: To access the “name ” prop in the child component, use the props key with an array. Set the first element in the array to “name.” Now, “name” will be “yourName” which will be passed into the component from the parent component.

<template>
<div class="component">
<p>Username: {{name}}</p>
{{logName()}}
</div>
</template>
<script>
export default {
props: ["name"],
methods: {
logName: function () {
console.log(this.name);
},
},
};
</script>

Passing Dynamic Props: You can dynamically pass a prop into a component by using a colon in front of the prop => :yourProp=”dataProperty”

<template>
<app-user-detail :name="name"></app-user-detail>
</template>
<script>
import UserDetail from "./UserDetail.vue";
export default {
data: function() {
return {
name: "Max",
};
},
components: {
appUserDetail: UserDetail,
},
};
</script>

Validating Props: In the child component, we are accepting a prop called “name.” We only want to accept props that are strings or arrays. We also require that this criteria is satisfied otherwise a warning will appear in the console. We can also set a default value for the prop if no prop is passed into the component. If the prop check does not pass, there will be a warning message in the console.

props: {
// name: [String, Array],
name: {
type: [String, Array],
required: false,
default: 'hello'
},
},

Emitting data to the parent component: In the child component, when this.$emit() is called, it will send a message called “nameWasReset” to the parent. The second argument is data that is passed to the parent. In our example, we pass this.name to the parent.

methods: {
myEmitter() {
this.$emit("nameWasReset", this.name);
},
},

In the parent component, we listen for the “nameWasReset” emission from the child component. We can access the information sent from the child component using $event.

<app-user-detail @nameWasReset="name = $event"></app-user-detail>

State Management: Moden front-end frameworks depend on state management. The data properties represent the state. Whenever there are changes to the data properties/state, the view will change.

To prevent prop drilling (having to pass prop from child to child to child), you can implement an event bus which is a component dedicated to maintaining the global state and listening for changes in the state. You can access the data and methods in the event bus in any component that you import this Vue instance into. Note, you can use a formal library like VueX to handle state management.

  1. In the main.js file, create a new Vue instance. We will dub it “eventBus.”
export const eventBus = new Vue({
data: {
a: "hello"
},
methods: {
b: function () {
console.log("hello");
}
}
});

2. Import the eventBus into another component (i.e. a sibling, parent, etc) which would be affected by the global state. Using the “created” life cycle hook, you can listen for the specific message which will fire a callback when the message is received.

import { eventBus } from "../main";created() {
eventBus.$on("ageWasEdited", (age) => {
this.userAge = age;
});

},

Slots: You can pass HTML markup into Vue components using slots. To do so, insert HTML markup between the parent component tags. In the child component, you can render the HTML using <slot></slot>. At the parent level, you can pass in dynamic data into the HTML markup. Note, however, you must style the HTML markup in the child component since we are using scoped styling.

You can also assign each slot in the parent so that you can use a specific HTML block in the child. In the example below, we write <h2 slot=’title’> so we can use the this HTML block in the child with <slot name=’title’></slot>.

Note, instead of using <h2 slot=’title’> you can use the following shorthand <h2 #title>

<!-- parent -->
<div class="col-sm-12">
<child-component>
<h2 slot='title'>Hello</h2>
<p slot='content'> {{ myData }} </p>
</child-component>
</div>
<!-- child -->
<template>
<div>
<slot name="title"></slot>
<hr />
<slot name="content"></slot>
</div>
</template>

Dynamic component rendering: Write a <component> in the template with an :is=’yourDynamicProperty’ attribute. Vue will render the component based on the value of ‘yourDynamicProperty’. In the example below, since ‘selectedComponent’ = ‘appQuote’ the Quote (appQuote = Quote in the components property) component will be rendered

<template>
<component :is="selectedComponent"></component>
</template>
export default {
name: "app",
data: function () {
return {
selectedComponent: "appQuote"
};
},
components: {
appQuote: Quote,
appAuthor: Author,
appNew: New

}
};

Keeping Components Alive: In the example above, if the component is dynamically changed from Quote to Author, the Quote component will be destroyed so all the local data stored in the Quote component will be lost. However, if we wrap the component in a <keep-alive> tag, the Quote component will persist (not be destroyed) when we change the component that is rendered. Therefore, if we re-render the Quote component in the future, all the data will remain.

Now, whenever you navigate to a <keep-alive> component, we can use the activated() life cycle hook. Whenever you navigate away from the <keep-alive> component, we can use the deactivated() life cycle hook.

<template>
<keep-alive>
<component :is="selectedComponent"></component>
</keep-alive>
</template>

Forms: Surround the form fields with <form @submit.prevent=’yourMethod’></form> to prevent the page from its default behavior of reloading. Upon submission, yourMethod will fire.

  • Text Input: Use v-model for two-way binding that will track all data changes that the user inputs. In the example below, we set the v-model to store the data in “userName.”
<form @submit.prevent="submitForm">
<input type="text" v-model="userName" />
</form>
<script>
export default {
data() {
return {
userName: "",
};
},
</script>
  • Checkboxes: Set the “name” property to the same value for each checkbox. Give each checkbox a unique “ value” attribute. Again, use v-model for two-way binding. In the example below, we use v-model to store the data in “interest” which must be an empty array for checkboxes. When a new checkbox item is clicked, it will be added to the array.
<form @submit.prevent='submitForm'>
<input name="interest" type="checkbox" v-model="interest" value="news" />
<input name="interest" type="checkbox" v-model="interest" value="tutorials" />
</form>
<script>
export default {
data() {
return {
interest: [],
};
},
</script>
  • Radio Buttons: To group radio buttons together, we set the “name” attribute to the same value for each radio button. Again, we use v-model for two-way binding. Give each radio button a unique “value.” We track the selected radio button with the “how” property.
<form @submit.prevent='submitForm'>
<input name="how" type="radio" v-model="how" value="blogs" />
<input name="how" type="radio" v-model="how" value="other" />
</form>
<script>
export default {
data() {
return {
how: null,
};
},
</script>
  • v-bind into custom components: You can use v-bind for 2-way binding with a child component. Using v-model, you can send data into a custom component. Inside of the custom component, you can access this data via the “value” props. You can send data back from the custom component to the parent by emitting an “input” keyword event using this.$emit(“input”, yourData). The parent will automatically be listening for an “input” event.
<template>
<rating-control v-model="rating"></rating-control>
</template>
<script>
export default {
data() {
return {
rating: "test"
};
}
};
</script>
------------------Rating Control Component-------------------------
<script>
export default {
props: ["value"],
methods: {
activate() {
this.$emit("input", 'red');
}
}
};
</script>

Routing: We will use Vue’s offical routing library called vue-router.

  1. npm install vue-router@next.
  2. In main.js import the createRouter method.
  3. Create a router object by calling the createRouter method and pass in an array of routes with the path and component keys. In the example below, if the user goes to localhost:8080/teams, he will be routed to the TeamsList component. It should be noted that you cannot embed a custom component (inside another component) that is also used as a routed component unless you set props=true
  4. Pass the router object into app.use()
//main.js
import { createApp } from "vue";
import { createRouter} from "vue-router";
import App from "./App.vue";
import TeamsList from "./components/teams/TeamsList";
import UsersList from "./components/users/UsersList";
const router = createRouter({
routes: [
{ path: "/teams", component: TeamsList, props=true },
{ path: "/users", component: UsersList, props=true },
],
});
const app = createApp(App);
app.use(router);
app.mount("#app");

5. Use <router-view> to render the component for our path. Therefore, <router-view> will render TeamsList when the user visits localhost:8080/teams

<template>
<router-view></router-view>
</template>

6. To make a link to this path use <router-link> as follows with the “to” attribute to direct the user to a given path. As a side note, a <router-link> is considered an <a> for css styling.

<template>
<router-link to='/users'></router-link>
<router-link to='/teams'></router-link>
</template>

7. You can programmatically send users to different paths using this.$router.push(‘/users’) which sends the user to the users page. You can also send the user to the previous or next page using this.$router.back() or this.$router.forward(), respectively.

Params: You can access the params via props if you set props=true in main.js. If the user is routed to a component containing params, you can access the params inside of the destination component using this.$route.params.yourParams. Of note, navigating to two pages with the same path but different params will not cause the page to render the params for the 2nd page because Vue caches the page for a given path and does not rerender unless the base path changes.

To force a rerender on the same path when the params change, you can use the watch when the route changes using watch() and .$route

//mains.js
const router = createRouter({
routes: [
{ path: "/teams/:teamId", component: TeamMembers },
],
});
//Custom Component
watch: {
$route(newRoute) {
console.log(newRoute);
},
},

Redirect:

//main.js
const router = createRouter({
routes: [
{ path: "/", redirect: '/teams' },
{ path: "/teams", component: TeamsList },
],
});

Catch All Route: If the user enters an invalid route, you can redirect the user to a specific page. Make sure your catch-all route is at the end of your routes otherwise it will fire everytime

const router = createRouter({
routes: [
{ path: "/", redirect: "/teams" },
{ path: "/teams", component: TeamsList },
{ path: "/:notFound(.*)", redirect: "/teams" },
],
});

Nested Routes: Note, you must add another <router-view> inside of the parent component to render the child route. In the example below, you need to add the <route-view> in the TeamsList component.

const router = createRouter({
routes: [
{ path: "/", redirect: "/teams" },
{
path: "/teams",
component: TeamsList,
children: [
{ path: ":teamId", component: TeamMembers, props: true },
],

},
],
});

Authentication: You can create private routes using authentication middleware.

  1. In main.js, use beforeEnter(to, from, next) {} which will fire before the user is routed. Any data stored in the meta property (in bold below) can be accessed in the “to” object in beforeEnter(to, from, next)
const router = createRouter({
routes: [
{
path: "/users",
meta: {
needsAuth: true
},

components: {
default: UsersList,
beforeEnter(to, from, next) {
console.log(to.meta.needsAuth);
next();
}

}
}
]
});

2. Inside of the component, you can call the beforeRouteEnter life cycle method

<script>
export default {
beforeRouteEnter(to, from, next) {
next();
},
};
</script>

vuex: Vue tracks local state (state within the component) based on the data returned from the “data” property in the Vue instance. Whenever the data changes, the component will rerender. To manage the global state, we use vuex which is the state management library supported directly by the Vue.js. In vuex, we have 4 main properties that are maintained in the “store”: state(), mutations, actions, and getters. For larger applications, you may organize your components’ properties in modules. Each module will have an encapsulated state. Please refer to vue documentaiton for more information on using modules.

  1. npm install vuex@next
  2. In main.js, import { createStore} from ‘vuex’
  3. Pass the store into your root vue instance: app.use(store)
import { createApp } from 'vue';
import { createStore } from 'vuex';
import App from './App.vue';
const store = createStore({
state() {
return {
counter: 0,
};
},
});
// creates a root vue instance
const app = createApp(App);
app.use(store);
app.mount('#app');

3. To access properties in the store within the <template> of your component, use $store.state.yourStoreProperty. To access properties within the <script> (i.e. methods, computed), use this.$store.yourStoreProperty.

<template>
<h3>{{ $store.state.counter }}</h3>
<button @click="addOne">Add 1</button>
</template>
<script>
export default {
methods: {
addOne() {
this.$store.counter++;
},
},
};
</script>

4. Mutations: For consistency across components, it is best to mutate the global state by using methods from the store. To do so, write methods inside of the “mutations” property of the store. By default, the global state is passed into these methods as the first argument. You can also pass data into the mutation method through the “payload” which is the second argument. Tip: You typically want to pass an object in the payload.

Then, you can call the mutation method inside the component using this.$store.commit(‘yourMutationMethod’, yourPayload). You can also use the following syntax this.$store.commit({ type: ‘yourMutationMethod’, yourPayloadProperty: yourPayloadValue}). Note mutations must be synchronous.

//in main.js
import { createApp } from 'vue';
import { createStore } from 'vuex';
import App from './App.vue';
const store = createStore({
state() {
return {
counter: 0,
};
},
mutations: {
increase(state, payload) {
state.counter += payload.value;
},
},

});
const app = createApp(App);
app.use(store);
app.mount('#app');
----------------------------------------------------------------------
//in your component
<template>
<button @click="addPayload">Add Payload</button>
</template>
<script>
export default {
methods: {
addPayload() {
this.$store.commit('increase', {
value: 3
});
},
// addPayload() {
// this.$store.commit({type: 'increase', value: 3});
// },
},
};
</script>

5. Getters: Getters are like computed properties in the store. You will typically use the getter in the computed property of your component. To access the getter in the computed property, this.$store.getters.yourGetter. Just like a computed property, you do not need to call the getter. The getters have access to the state and other getters.

Note, you can also import the getters into the computed property of your component using …mapGetters([‘yourGetter’]).

//main.js
import { createApp } from 'vue';
import { createStore } from 'vuex';
import App from './App.vue';
const store = createStore({
state() {
return {
counter: 0,
};
},
getters: {
incrementGetter(state) {
return state.counter + 1;
},
printCounter(state, getters) {
console.log(state.counter);
console.log(getters.incrementGetter);
},
},

});
const app = createApp(App);
app.use(store);
app.mount('#app');
--------------------------------------------------------------------
//in your component
<template>
<h3>{{ counterGetter }}</h3>
<h3>{{ incrementGetter }}</h3>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters(['finalCounter'])
counterGetter() {
return this.$store.getters.printCounter;
},
},
};
</script>
<style>
</style>

6. Actions: Since mutations methods cannot have asynchronous functions, we use “actions” whenever we use asynchronous functions. The action accepts a “context” argument. The context has a commit methods that has access the mutation methods.

In the methods property of your component, you can create a method that calls the asynchronous action you created. You could also use the action directly by importing mapActions then using …mapActions[‘yourAction’] in the methods. When calling yourAction, you can pass a payload as an object.

Alternatively, you can use this.$store.dispatch method to call an action from the store. The “dispatch” method handles promises which enables you to use asynchronous actions.

//main.js
import { createApp } from "vue";
import { createStore } from "vuex";
import App from "./App.vue";
const store = createStore({
state() {
return {
counter: 0
};
},
mutations: {
increase(state, payload) {
state.counter += payload.value;
}
},
actions: {
actionIncrement(context) {
setTimeout(function () {
// call the increase mutation
context.commit("increase", payload);
}, 2000);
}
}

});
const app = createApp(App);
app.use(store);
app.mount("#app");
--------------------------------------------------------------------
//in your component
<template>
<button @click="addAction">Add Action (10)</button>
<button @click="actionIncrement({ value: 10 })">
Add actionIncrement (10)
</button>
</template>
<script>
import { mapActions } from 'vuex';
export default {
methods: {
...mapActions(['actionIncrement']),
addAction() {
this.$store.dispatch({
type: 'actionIncrement',
value: 10,
});

},
// addAction2() {
// this.$store.dispatch('actionIncrement', { value: 10 });
// },
},
};
</script>

7. Computed: You will likely want to rerender/react to a certain property change in the store/state. To do so, store the desired property from the store in the “computed” portion of your component using this.$store.state.yourProperty. In the example below, whenever another component causes the “counter” property in the store to change, the value will change in the component below. If you store the property in “data,” the property will not change if the property in the store changes.

<template>
<div>{{ keepCount }}</div>
</template>
<script>
export default {
computed: {
keepCount() {
return this.$store.state.counter;
}
}
};
</script>

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store