Building a Real-World Web App With Vue.js and Firebase

Alec Plummer

Last updated Jul 20, 2020

Developers are adopting Vue.js at a rapid pace. Boasting over 167,000 stars on GitHub, this modern, modular JavaScript library has become a compelling option for web app development. This adoption is in no small part due to Vue.js being so easy to integrate into an existing project. It's fast. It's extremely powerful. And it allows you to build a dynamic, scalable, maintainable single-page application from scratch.

We just released a major update in July 2020! This release includes the current Vue CLI, async/await instead of promise chaining, and a structure overhaul incorporating updated best practices. The flow remains unchanged with some updates to match the current Firebase SDK. All code for the rewrite can be found in the project's master branch, while the old code remains in original-deprecated for posterity.

Changelog:

  • Complete rewrite of existing project
  • Async/Await instead of promise chaining
  • More components vs. same file functionality
  • Leveraging more actions vs. in-component requests to handle Firebase methods
  • Simplified Firebase integration

Savvy was an early adopter among agencies, using Vue.js to power web apps for Levi's and Pocket Prep. And now we’re leveraging our experience to help developers who are just getting started with Vue.js. Since the best way to learn new concepts is by trying them out in real-world scenarios, we’ve created a sample project that ties together a number of Vue concepts while going beyond a basic to-do app. In this resource you'll not only learn many of the core concepts of working with Vue.js, you'll also get hands-on experience creating a production-ready app.

Why Vue.js?

Vue.js is a JavaScript framework that allows you to easily render dynamic data to the DOM, bind data to DOM elements, and manage/maintain your app's state (local storage) without requiring a user to reload the browser. The growing popularity of Vue.js cannot be understated. We prefer it because it's lightweight, modular, and calls for minimal configuration. It's also extremely fast and has a low file size. Developers can easily drop it into any project or existing framework.

What's more, Vue.js isn't backed by any major company or organization. While that kind of backing comes with its own positives, we feel Vue.js works better as an open-source, third-party framework carrying fewer motives or biases. Normally we would be concerned about the longevity of a product without that type of support. However, we believe Vue is here to stay thanks to its popularity and the activity around it so far.

We won't dive too deep into the benefits of using Vue.js; there are already a number of solid resources that go in-depth on this very topic. Check out Vue.js' own comparison of itself with other frameworks to get a feel for the advantages and differences between the more popular options. Hacker Noon offers a roundup of opinions about Vue.js versus other platforms from prominent developers. You'll also want to stay up to date on Vue.js features as well as its development roadmap on the Vue news site.

What We Will Be Making - Vuegram

We created Vuegram, a simple social media web app, to help you get hands-on experience with the core features of Vue.js. Vuegram allows users to create an account, log in, publish posts, and interact with other users by adding comments and “liking” posts all in realtime. Although there are multiple ways to create an app like Vuegram, the code in this tutorial illustrates some of the common patterns found in production-ready apps made with Vue.js.

In constructing Vuegram, you'll learn how to:

  • Start a production-ready project using Vue CLI.
  • Manage state for your app using Vuex. You'll also understand why a central store is important for building large-scale apps.
  • Maintain reactivity within your app by utilizing built-in Vuex features.
  • Authenticate routes with vue-router.
  • Read/write to a database using Firebase's Cloud Firestore.
  • Set up authentication using Firebase.
  • Leverage components, one of the more powerful features of Vue.js.
  • Familiarize yourself with core Vue.js concepts.

These concepts and patterns will enable you to get up and running with Vue.js, serving as a foundation to build more complex and scalable web apps. They'll give you a greater sense of how to scale your Vue app and manage a lot of files easily. You'll also be positioned to prevent pitfalls before they happen, potentially saving loads of development time.

Head over to GitHub to access the Vuegram project and use it to follow along with the rest of this tutorial. Now let's get started!

Initial Setup With Vue CLI

For your reference, please see the Vue CLI documentation. Follow these steps to install and initialize via Vue CLI 3:

Step 1: Install Vue CLI 3.

npm install -g @vue/cli

Step 2: Initialize your project with Vue CLI 3.

vue create vue-app

Step 3: Configure Vue CLI 3.

Choose "Manually select features." Then select "babel," "Router," "Vuex," "CSS Pre-processors," and "linter/formatter." You will be prompted to select your preprocessor. Choose whichever you like; we use sass via node instead of dart.

Since we will be using the default linter for error prevention only, you might want to add a rule to your .eslintrc.js file that turns off a common linter error ('value' is defined but never used).

// the code - add the 'vue/no-unused-components' rule
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/no-unused-components': 'off'
}

Step 4: Serve up a localhost.

Once everything is installed, navigate to the root folder and run `npm run serve` in the terminal to serve up a localhost.

File Structure Overview

The current version of the Vue CLI sets up your project in a very specific way that allows for scalable and familiar file structure across projects. The first thing we will want to do is create all of our views within the views folder: Login.vue, Dashboard.vue, and Settings.vue. The basic structure of a Vue single file component includes your markup, script, and style tags.

<template>
  <div>

  </div>
</template>

<script>
export default {

}
</script>

<style lang="scss" scoped>

</style>

Next we want to set up our router so these views are available when we visit those routes. The current version of the Vue CLI leverages code splitting, which is a great way to reduce initial load times for your project; the code is only served up when it’s needed.

import Vue from 'vue'
import VueRouter from 'vue-router'
import Dashboard from '../views/Dashboard.vue'

Vue.use(VueRouter)

  const routes = [
  {
    path: '/',
    name: 'Dashboard',
    component: Dashboard
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue')
  },
  {
    path: '/settings',
    name: 'settings',
    component: () => import(/* webpackChunkName: "settings" */ '../views/Settings.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

Let’s clean up the project a bit by removing starter boilerplate files.

  • Home.vue and About.vue in the /views folder
  • assets/logo.png
  • components/HelloWorld.vue

Clean up project App.vue by removing the starter code, otherwise our app will throw errors trying to render components that don’t exist.

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

State Management With Vuex (And Props)

Deciding how to handle state management is critical when starting a project with Vue.js. One solution, Vuex, is a pattern and library that handles state management for Vue.js. It provides the ability to share data across all components from a single place.

You could also create your own system for handling state management using props. Props are part of Vue.js; they pass data to a child component. For example, say you have a settings view. In that settings view you have a modal that functions as its own component because you use it in multiple places. In this case, you need to pass data from the settings view (parent) to the modal (child). You would use props to pass this data.

Depending on your project size and complexity, using Vuex to create a central store for all of your reactive data makes it easier to pass data to props regardless of their hierarchy. That's because a child component speaks directly to the Vuex store object. If you decided to accomplish this using props alone, you would need to pass data down the chain instead of directly to the component. Likewise, to pass data from a child component up two or more levels, you would need to pass that data up the chain instead of passing directly to the store. In that case, your parent component would then have those updates.

You may be tempted to forgo Vuex entirely and rely on props when you just need to pass data to a child component (example: modals, alerts, etc.). We don't recommend it, though, as it is bad practice to pollute the store with this kind of simple downstream data flow. We prefer Vuex because it provides an easy and predictable pattern to use when updating parts of an app. Vuex keeps data reliable as it provides a single point of truth, whereas with props, you can have one component with a user property that's been changed and another component that remains unchanged. Keeping track of different versions of the same component can easily lead to chaos.

With that context, it should be no surprise that we'll use Vuex for managing the entirety of this app's local state.

Setting Up a Database With Firebase Cloud Firestore

We're big fans of Firebase. Savvy uses Firebase as one of its backends for web and native apps, as well as for real-time updates, user authentication, static hosting, push notifications, and more. In this section you'll learn how to use one of our favorite Firebase tools, Cloud Firestore, to set up a back-end database and integrate it within a Vue.js project.

Cloud Firestore is one of two Firebase tools that allows developers to set up and provision a back-end database as a service. Released in 2017, Cloud Firestore provides a different architecture and focuses on different priorities than the older Firebase offering, Realtime Database. It's important to choose the best database option for your project before beginning. We go in-depth on the benefits of using Cloud Firestore and compare it to Firebase's other backend offering, Realtime Database, in Choosing a Firebase Database for Your App.

Note that while Realtime Database would be more cost efficient in a real production app similar to the Vuegram project, we chose to integrate Cloud Firestore for the backend as practice in setting up the new service in a new Vue.js project.

For this part of the tutorial, follow along with the Firebase Console and the firebase.js file in the Vuegram GitHub repo.

Step 1: Create your Firebase project.

To create a Firebase project, go to the Firebase Console and click “add project.” Give it a name, then click “create project.” Click “Add Firebase to your web app.” Be sure to copy this code. You will need it later when you set up the firebase.js file.

Click “Authentication” in the left-hand navigation, then “Set sign-up method,” then “email/password.” Enable and save.

Select “Database.” Click “Get started” for Cloud Firestore (currently in beta). Start in test mode and enable so anyone can read/write to the database. You will add basic security rules later.

Step 2: Install package.

Open your command line and install Firebase.

npm i firebase

Step 3: Create the firebase.js file.

Import Firebase and Cloud Firestore, and set up Firebase configuration. The code you copied when you created your project will replace the current configuration code. Create your basic utility methods/variables, then update the firebase.js file with your own Firebase credentials.

Since Cloud Firestore is in beta, changes will happen from time to time.

Create your references to the following collections: users, posts, comments, and likes. Now, export!

import * as firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'

// firebase init - add your own config here
const firebaseConfig = {
  apiKey: '',
  authDomain: '',
  databaseURL: '',
  projectId: '',
  storageBucket: '',
  messagingSenderId: '',
  appId: ''
}
firebase.initializeApp(firebaseConfig)

// utils
const db = firebase.firestore()
const auth = firebase.auth()

// collection references
const usersCollection = db.collection('users')
const postsCollection = db.collection('posts')
const commentsCollection = db.collection('comments')
const likesCollection = db.collection('likes')

// export utils/refs
export {
  db,
  auth,
  usersCollection,
  postsCollection,
  commentsCollection,
  likesCollection
}

Step 4: Update the main.js file to handle authentication state changes (like the page reload and login).

Import your Firebase configuration and set up the Firebase method onAuthStateChanged. This ensures Firebase initializes before loading the app when a user refreshes a page.

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { auth } from './firebase'

Vue.config.productionTip = false

let app
auth.onAuthStateChanged(() => {
  if (!app) {
    app = new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app')
  }
})

With this complete, you're ready to tackle authentication by setting up vue-router.

Authenticating Routes With vue-router

The next step is learning what vue-router is, how to use it to authenticate different routes within your app, and how to set it up.

The officially supported vue-router let’s you define how users interact and access the different views in your app. Since Vue.js can create single-page applications, there's no need to reload your app when switching between views. This is handled by telling Vue.js which view to show and when to show it. For example, in Vuegram you'll want to lock down all views besides the login screen if a user is not authenticated.

For this part of the tutorial, follow along with the index.js file in the Vuegram GitHub repo.

Step 1: Import Firebase auth.

import Vue from 'vue'
import VueRouter from 'vue-router'
import Dashboard from '../views/Dashboard.vue'
import { auth } from '../firebase'

Step 2: Add a meta object to routes we want to lock down.

This allows us to flag the routes required for a user to be authenticated. We do this by adding an object named meta with a property requiresAuth: true

const routes = [
  {
    path: '/',
    name: 'Dashboard',
    component: Dashboard,
    meta: {
      requiresAuth: true
    }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import( /* webpackChunkName: "login" */ '../views/Login.vue')
  },
  {
    path: '/settings',
    name: 'settings',
    component: () => import( /* webpackChunkName: "settings" */ '../views/Settings.vue'),
    meta: {
      requiresAuth: true
    }
  }
]

Step 3: Set up the beforeEach navigation guard.

Check if the route exists and requires authentication. Next, create a reference to the current user and authenticate routes.

If the route has the requiresAuth meta property set to true and the user is not logged in, redirect them to the login view. If the route has the requiresAuth meta property set to true and and the user is logged in, send them to the route they are attempting to visit. Or else, you can send them to the route they are trying to visit; this only fires if the route doesn't require authentication.

router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(x => x.meta.requiresAuth)

  if (requiresAuth && !auth.currentUser) {
    next('/login')
  } else {
    next()
  }
})

Step 4: Test routes manually to check if authentication is working.

All of your routes should now redirect to /login because you aren't logged in.

Remember to use precise naming conventions when completing this part of the project, particularly when handling imports, routes, route guards, and export. A common mistake is mishandling user authentication. Make sure Firebase loads before routing your logic.

With this complete, let's move on to creating the app's login view.

Creating the Login and Signup View

In this section not only will you create a functioning login/signup view for your app, you will also learn some basic concepts of Vue.js and Vuex, and get up and running with single component files. We’ll be introducing the concepts of:

For Vue.js:

For Vuex:

You'll also learn how to change security rules for users with the Firebase console.

Quick note: We keep all logic for the login authentication in this view to show the different patterns. For settings you will abstract all logic to store/index.js.

For this part of the tutorial, follow along with the login.vue file in the Vuegram GitHub repo.

Step 1: Import SCSS to project.

The directory structure for the project's CSS is src/assets/SCSS. Grab all three files from the GitHub repo and import to main.js.

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { auth } from './firebase'
import './assets/scss/app.scss'

Step 2: Set up markup.

Here's what your basic code will look like with just the login:

<template>
  <div id="login">
    <section>
      <div class="col1">
        <h1>Vuegram</h1>
        <p>Welcome to the <a href="https://savvyapps.com/" target="_blank">Savvy Apps</a> sample social media web app powered by Vue.js and Firebase.
          Build this project by checking out The Definitive Guide to Getting Started with Vue.js</p>
      </div>
      <div class="col2">
        <form>
          <h1>Welcome Back</h1>
          <div>
            <label for="email1">Email</label>
            <input type="text" placeholder="[email protected]" id="email1" />
          </div>
          <div>
            <label for="password1">Password</label>
            <input type="password" placeholder="******" id="password1" />
          </div>
          <button class="button">Log In</button>
          <div class="extras">
            <a>Forgot Password</a>
            <a>Create an Account</a>
          </div>
        </form>
      </div>
    </section>
  </div>
</template>

Add directives to the form inputs and create the corresponding data properties. The v-model is how you bind data in Vue.js. You'll also want to add a .trim modifier, as modifiers extend the functionality of directives in Vue.

Also add submit.prevent to the form so the form doesn't submit. This is important as the default behavior of a form submission is a page reload. You want to hijack this event to save to the database without a page reload. This is similar to how AJAX works with form submissions. Your code should now look like this:

<template>
  <div id="login">
    <section>
      <div class="col1">
        <h1>Vuegram</h1>
        <p>Welcome to the <a href="https://savvyapps.com/" target="_blank">Savvy Apps</a> sample social media web app powered by Vue.js and Firebase.
          Build this project by checking out The Definitive Guide to Getting Started with Vue.js</p>
      </div>
      <div class="col2">
        <form @submit.prevent>
          <h1>Welcome Back</h1>
          <div>
            <label for="email1">Email</label>
            <input v-model.trim="loginForm.email" type="text" placeholder="[email protected]" id="email1" />
          </div>
          <div>
            <label for="password1">Password</label>
            <input v-model.trim="loginForm.password" type="password" placeholder="******" id="password1" />
          </div>
          <button class="button">Log In</button>
          <div class="extras">
            <a>Forgot Password</a>
            <a>Create an Account</a>
          </div>
        </form>
      </div>
    </section>
  </div>
</template>

<script>
export default {
  data() {
    return {
      loginForm: {
        email: '',
        password: ''
      }
    }
  }
}
</script>

Step 3: Create the login logic.

Add a click event to our login button.

<button @click="login()" class="button">Log In</button>

Go ahead and create the login method. We will be dispatching an action to Vuex to handle user login.

methods: {
  login() {
    this.$store.dispatch('login', {
      email: this.loginForm.email,
      password: this.loginForm.password
    })
  }
}

To set up the store.js file for login, you need to understand a few more key concepts. State is an object that holds all of your global objects/arrays/variables for your app. It allows you to set the userProfile object as a global object for the app, so you can access its properties on multiple views.

Make sure to import the firebase.js file and our router.

import Vue from 'vue'
import Vuex from 'vuex'
import * as fb from '../firebase'
import router from '../router/index'

Add the property to the state object.

state: {
  userProfile: {}
},

Create the login and fetchUserProfile actions.

actions: {
  async login({ dispatch }, form) {
    // sign user in
    const { user } = await fb.auth.signInWithEmailAndPassword(form.email, form.password)

    // fetch user profile and set in state
    dispatch('fetchUserProfile', user)
  },
  async fetchUserProfile({ commit }, user) {
    // fetch user profile
    const userProfile = await fb.usersCollection.doc(user.uid).get()

    // set user profile in state
    commit('setUserProfile', userProfile.data())
    
    // change route to dashboard
    router.push('/')
  }
}

Create the setUserProfile mutations to update the user profile in the state.

mutations: {
  setUserProfile(state, val) {
    state.userProfile = val
  }
}

Your finished code should look like:

import Vue from 'vue'
import Vuex from 'vuex'
import * as fb from '../firebase'
import router from '../router/index'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    userProfile: {}
  },
  mutations: {
    setUserProfile(state, val) {
      state.userProfile = val
    }
  },
  actions: {
    async login({ dispatch }, form) {
      // sign user in
      const { user } = await fb.auth.signInWithEmailAndPassword(form.email, form.password)

      // fetch user profile and set in state
      dispatch('fetchUserProfile', user)
    },
    async fetchUserProfile({ commit }, user) {
      // fetch user profile
      const userProfile = await fb.usersCollection.doc(user.uid).get()

      // set user profile in state
      commit('setUserProfile', userProfile.data())
      
      // change route to dashboard
      router.push('/')
    }
  }
})

Step 4: Create the signup logic.

To add the signup, you first need to add your markup and data binding to the signup form inputs like you did for the login form.

<form @submit.prevent>
  <h1>Get Started</h1>
  <div>
    <label for="name">Name</label>
    <input v-model.trim="signupForm.name" type="text" placeholder="Savvy Apps" id="name" />
  </div>
  <div>
    <label for="title">Title</label>
    <input v-model.trim="signupForm.title" type="text" placeholder="Company" id="title" />
  </div>
  <div>
    <label for="email2">Email</label>
    <input v-model.trim="signupForm.email" type="text" placeholder="[email protected]" id="email2" />
  </div>
  <div>
    <label for="password2">Password</label>
    <input v-model.trim="signupForm.password" type="password" placeholder="min 6 characters" id="password2" />
  </div>
  <button @click="signup()" class="button">Sign Up</button>
  <div class="extras">
    <a>Back to Log In</a>
  </div>
</form>

Then update the data object and add the signupForm and its properties.

// add signup form to data() 
signupForm: {
  name: '',
  title: '',
  email: '',
  password: ''
}

Step 5: Create the signup method.

Create the signup method.

signup() {
  this.$store.dispatch('signup', {
    email: this.signupForm.email,
    password: this.signupForm.password,
    name: this.signupForm.name,
    title: this.signupForm.title
  })
}

In our store/index.js file, we need to create the action. The signup method is handled a little differently. First you need to create a user in Firebase, then create a user profile that you can manipulate. So go ahead and sign up with Firebase, then create a user object.

async signup({ dispatch }, form) {
  // sign user up
  const { user } = await fb.auth.createUserWithEmailAndPassword(form.email, form.password)

  // create user profile object in userCollections
  await fb.usersCollection.doc(user.uid).set({
    name: form.name,
    title: form.title
  })

  // fetch user profile and set in state
  dispatch('fetchUserProfile', user)
}

Step 6: Add the ability to toggle between login and signup.

Use v-if/v-else, then add a click event toggleForm on the “Create an Account” and “Back to Log In” buttons to conditionally hide/show the correct form. The code below shows you where to add this.

// add v-if/v-else to forms in markup
<form v-if="showLoginForm" @submit.prevent>
  // login form content
  // .extras div
  <a @click="toggleForm()">Create an Account</a>
</form>
<form v-else @submit.prevent>
  // signup form content
  // .extras div
  <a @click="toggleForm()">Back to Log In</a>
</form>

Update the data object to include the showLoginForm property and create the toggleForm method.

// add property to data()
showLoginForm: true

// add toggle form method
toggleForm() {
  this.showLoginForm = !this.showLoginForm
}

Now you can toggle between login and signup.

You will want to update the CSS so the forms line up when toggled. To do this, bind a dynamic class to the DOM element that will add/remove the class based on the value of the showLoginForm property.

<div :class="{ 'signup-form': !showLoginForm }" class="col2">

Go ahead and test your signup and login. They work!

If they don't work, check your console for errors. Double check that Firebase is imported to your store/index.js file and the Firebase import path is correct.

Step 7: Create forgot password markup and logic.

You'll want to update the markup with a click event, add a property to data(), and add a method to toggle forgot password. Optional: you can edit the password reset email template through the Firebase console.

// markup
<div class="extras">
  <a @click="togglePasswordReset()">Forgot Password</a>
  // toggle form
</div>

// add to data()
showPasswordReset: false

// add method
togglePasswordReset() {
  this.showPasswordReset = !this.showPasswordReset
}

Next create a PasswordReset.vue file in your components folder. The basic markup should look like this:

<template>
  <div class="modal">
    <div class="modal-content">
      <div @click="$emit('close')" class="close">close</div>
      <h3>Reset Password</h3>
      <div v-if="!showSuccess">
        <p>Enter your email to reset your password</p>
        <form @submit.prevent>
          <input v-model.trim="email" type="email" placeholder="[email protected]" />
        </form>
        <button @click="resetPassword()" class="button">Reset</button>
      </div>
      <p v-else>Success! Check your email for a reset link.</p>
    </div>
  </div>
</template>

Import auth from Firebase and set up the basic data bindings and resetPassword method.

<script>
import { auth } from '@/firebase'

export default {
  data() {
    return {
      email: '',
      showSuccess: false
    }
  },
  methods: {
    async resetPassword() {
      // reset logic
    }
  }
}
</script>

Now to create the methods to handle the password logic. Use resetPassword to leverage the email entered by the user to send a reset password email. You’ll also want to add some error handling incase anything goes wrong.

// add error message to markup
<p>Enter your email to reset your password</p>
<form @submit.prevent>
  // input
</form>
<p v-if="errorMsg !== ''" class="error">{{ errorMsg }}</p>
<button @click="resetPassword()" class="button">Reset</button>

// add property to data()
errorMsg: ''

// add logic to method
async resetPassword() {
  this.errorMsg = ''

  try {
    await auth.sendPasswordResetEmail(this.email)
    this.showSuccess = true
  } catch (err) {
    this.errorMsg = err.message
  }
}

Once complete, we need to import our component into Login.vue. We also want to add a way to show/hide the reset modal.

// add component at top
<template>
  <div id="login">
    <PasswordReset v-if="showPasswordReset" @close="togglePasswordReset()"></PasswordReset>
    
// import into file
import PasswordReset from '@/components/PasswordReset'

// add to vue instance
export default {
  components: {
    PasswordReset
  },

With your login/signup views and all login/signup logic complete, check this code in GitHub to double check if you're getting any errors.

Step 8: Implement security.

Before you move on, let's add some basic security rules. Your app will probably require more involved rules, but for now we’re OK with making the app readable by anyone and writable by logged-in users.

Go to your project's Firebase console and click on “Database,” then select the “Rules” tab at the top next to “Data.” Then update your security rules and click “Publish.”

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read;
      allow write: if request.auth.uid != null
    }
  }
}

Now it's time to tackle Vuegram's navigation.

Building the Navigation With Vue Components

In this section you'll create the navigation for your project while learning about Vue components, code that you can re-use across different views. Mentioned in the Vuex/state management section, components are one of the more powerful features of Vue.js. They provide the ability to create and break your project into reusable pieces of code with a specific function.

In Vuegram, we create a navigation component to prevent duplicate code across all views. Please note that in this case, you add the code to one place. You can also conditionally render to hide/show the navigation component based on a user's authentication, giving users a way to navigate the app.

For this part of the tutorial, follow along with the SiteNav.vue file in the Vuegram GitHub repo.

Step 1: Update the markup on the SiteNav.vue file.

You need to add a way for users to navigate the web app. Note that <router-link to=”/”> is the same thing as <a href=”/”>.

<template>
  <header>
    <section>
      <div class="col1">
        <router-link to="/">
          <h3>Vuegram</h3>
        </router-link>
        <ul class="inline">
          <li>
            <router-link to="/">Dashboard</router-link>
          </li>
          <li>
            <router-link to="/settings">Settings</router-link>
          </li>
          <li><a>logout</a></li>
        </ul>
      </div>
    </section>
  </header>
</template>

Step 2: Create the logout method

Let's create a method to log out users called logout. You will also create an action named logout in store/index.js.

// markup
<li><a @click="logout()">logout</a></li>

// create method and dispatch action
export default {
  methods: {
    logout() {
      this.$store.dispatch('logout')
    }
  }
}

Add that like so:

async logout({ commit }) {
  await fb.auth.signOut()

  // clear userProfile and redirect to /login
  commit('setUserProfile', {})
  router.push('/login')
}

Your completed Navigation.vue code should look like:

<template>
  <header>
    <section>
      <div class="col1">
        <router-link to="/">
          <h3>Vuegram</h3>
        </router-link>
        <ul class="inline">
          <li>
            <router-link to="/">Dashboard</router-link>
          </li>
          <li>
            <router-link to="/settings">Settings</router-link>
          </li>
          <li><a @click="logout()">logout</a></li>
        </ul>
      </div>
    </section>
  </header>
</template>

<script>
export default {
  methods: {
    logout() {
      this.$store.dispatch('logout')
    }
  }
}
</script>

Step 3: Update App.vue.

Import nav to app.vue, then import mapState to get userProfile. mapState is a Vuex helper that allows you to access properties on your state object without having to type them manually each time you need to reference them.

Set up app.vue to use the navigation component.

<template>
  <div id="app">
    <SiteNav v-if="showNav"></SiteNav>
    <router-view/>
  </div>
</template>

<script>
import { mapState } from 'vuex'
import SiteNav from '@/components/SiteNav'

export default {
  components: {
    SiteNav
  },
  computed: {
    ...mapState(['userProfile']),
    showNav() {
      return Object.keys(this.userProfile).length > 1
    }
  }
}
</script>

Now test your logout. It works!

Keep in mind that if you use the same functionality in multiple places, it's best to create a component and stick to DRY (don't repeat yourself) principles of programming. Also, when importing components, make sure the path is correct and tell your Vue instance to use a component:

components: { YourComponent }

You might notice that if you log back in and refresh the browser, the navigation goes away. Why is that? Let's see.

Handling User State on Page Reload

As the app currently stands, when a user reloads the page, all of the local state disappears. In this section, you'll learn how to fix that, making sure the app doesn't log out a user if for some reason he or she reloads the page. You'll then grab that user's data to repopulate the app state.

An easy way around this issue is to use Firebase's built-in method onAuthStateChanged. This watches the authentication state of your user on page reload, sets the user state, and fetches the user's profile. Let's implement this in one easy step.

You'll find the code for this part of the tutorial in the store.js file in the Vuegram GitHub repo.

Step 1: Update main.js.

The only thing you need to do to fix this is update the main.js file to dispatch an action on a page reload if a user is logged in. Check out how this is handled with fb.auth.onAuthStateChanged(user => {}) and if (user) { // update state } in the code snippet below:

let app
auth.onAuthStateChanged(user => {
  if (!app) {
    app = new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app')
  }

  if (user) {
    store.dispatch('fetchUserProfile', user)
  }
})

Now log in and reload the page. Your navigation should appear as intended. The last thing we need to do is update our store/index.js file to accommodate new logic. If the user reloads the page on /settings, for example, we don’t want to redirect them to the dashboard.

async fetchUserProfile({ commit }, user) {
  // fetch user profile
  const userProfile = await fb.usersCollection.doc(user.uid).get()

  // set user profile in state
  commit('setUserProfile', userProfile.data())

  // change route to dashboard
  if (router.currentRoute.path === '/login') {
    router.push('/')
  }
}

Developing the Dashboard

In this section you'll learn how to leverage the following Vue.js items while creating a functioning dashboard for your app.

For this part of the tutorial, follow along with the dashboard.vue file in the Vuegram GitHub repo.

Step 1: Set up the base markup.

Like so:

<template>
  <div id="dashboard">
    <section>
      <div class="col1">
        <div class="profile">
          <h5>n</h5>
          <p></p>
          <div class="create-post">
            <p>create a post</p>
            <form @submit.prevent>
              <textarea></textarea>
              <button class="button">post</button>
            </form>
          </div>
        </div>
      </div>
      <div class="col2">
        <div>
          <p class="no-results">There are currently no posts</p>
        </div>
      </div>
    </section>
  </div>
</template>

Step 2: Add the ability to create a post.

Let's give users the ability to create a post. Update the markup in the Dashboard.vue file. You also need to get access to the userProfile object in the state. Use the mapState method combined with the spread operator to pull this object into the dashboard component.

<script>
import { mapState } from 'vuex'

export default {
  data() {
    return {

    }
  },
  computed: {
    ...mapState(['userProfile'])
  },
  methods: {

  }
}
</script>

Update the markup to bind data to element properties. On the submit button, add the createPost method and :disabled="post.content == ''" to disable the button until the user has entered some content. Note where this is located here:

<template>
  <div id="dashboard">
    <section>
      <div class="col1">
        <div class="profile">
          <h5>{{ userProfile.name }}</h5>
          <p>{{ userProfile.title }}</p>
          <div class="create-post">
            <p>create a post</p>
            <form @submit.prevent>
              <textarea v-model.trim="post.content"></textarea>
              <button @click="createPost()" :disabled="post.content === ''" class="button">post</button>
            </form>
          </div>
        </div>
      </div>
      <div class="col2">
        <div>
          <p class="no-results">There are currently no posts</p>
        </div>
      </div>
    </section>
  </div>
</template>

Update the data object with post : { content: '' } and create the createPost method.

export default {
  data() {
    return {
      post: {
        content: ''
      }
    }
  },
  computed: {
    ...mapState(['userProfile'])
  },
  methods: {
    createPost() {
      this.$store.dispatch('createPost', { content: this.post.content })
      this.post.content = ''
    }
  }
}

Create the action in store/index.js.

async createPost({ state, commit }, post) {
  await fb.postsCollection.add({
    createdOn: new Date(),
    content: post.content,
    userId: fb.auth.currentUser.uid,
    userName: state.userProfile.name,
    comments: 0,
    likes: 0
  })
}

Once you've completed the above steps, create a test post and click the “post” button. Success!

Step 3: Fetch all posts and render to DOM.

First we need to change the way we export our store so we can access it from within Firebase methods.

// we will need access to the store so we need to change around how its exported
const store = new Vuex.Store({
  // apps state
})

export default store

You need a way to display all posts to the user. This can be handled in store/index.js on page load/login/signup. Set up a query snapshot in store/index.js; this will update in real time as changes are made to the postsCollection in the database.

// realtime firebase connection
fb.postsCollection.orderBy('createdOn', 'desc').onSnapshot(snapshot => {
  // logic goes here
})

Use orderBy() to prompt the posts to come back newest first. Next, loop through the array of objects, grab the data from the object, and assign an ID so you can look up that post for adding comments/liking. Push to the postsArray and commit a mutation to update the state:

// realtime firebase connection
fb.postsCollection.orderBy('createdOn', 'desc').onSnapshot(snapshot => {
  let postsArray = []

  snapshot.forEach(doc => {
    let post = doc.data()
    post.id = doc.id

    postsArray.push(post)
  })

  store.commit('setPosts', postsArray)
})

Add an empty array property to the state object named posts: []. Create the mutation to set the posts with `setPosts(state, val) { state.posts = val }.

// state 
state: {
  userProfile: {},
  posts: []
}

// mutation
setPosts(state, val) {
  state.posts = val
}

Now that you have the posts in the app's state, you need to render them out to the DOM. To do this, you need to bind out posts to the dashboard component so you can access them in the template. In computed properties, add 'posts' to mapState like so:

...mapState(['userProfile', 'posts'])

Next, update the template to display the posts using v-for. This is Vue's directive to render lists.

Update the markup and check to see if you have posts using v-if/v-else. If there are no posts, you should see a “no posts” message. Add the v-for directive to our div with the class “post” to loop through our posts array and render the object to the DOM. You can now see all posts. Create a new post and you will see it automatically update the DOM without a page reload.

<div v-if="posts.length">
  <div v-for="post in posts" :key="post.id" class="post">
    <h5>{{ post.userName }}</h5>
    <span>{{ post.createdOn }}</span>
    <p>{{ post.content }}</p>
    <ul>
      <li><a>comments {{ post.comments }}</a></li>
      <li><a>likes {{ post.likes }}</a></li>
      <li><a>view full post</a></li>
    </ul>
  </div>
</div>
<div v-else>
  <p class="no-results">There are currently no posts</p>
</div>

Now to include Vue filters.

You need to format the date and trim the length of extra long posts. To do this, leverage the power of custom filters in Vue. First, install moment using npm or yarn.

npm i moment

Next, import moment into the dashboard component below.

import moment from 'moment'

Then create your filters. Do this by adding a filters object, filters: {}. Add this under your method's object. Next, create two filters: formatDate(val) and trimLength(val). The val parameter is the property you want to manipulate. In this instance, pass the post's date and the post's content to their respective filters.

filters: {
  formatDate(val) {
    if (!val) { return '-' }
    
    let date = val.toDate()
    return moment(date).fromNow()
  },
  trimLength(val) {
    if (val.length < 200) { return val }
    return `${val.substring(0, 200)}...`
  }
}

Add them to the markup like this:

{{ fullPost.createdOn | formatDate }}
{{ post.content | trimLength }}

Now you can see your filters working perfectly. The date is formatted nicely and post content has been truncated.

And with that, you're all done with post logic! Open a new tab/browser, create another account, and test by creating new posts on both users to watch how they update.

Step 4: Implement comments.

Now that you've created all of the logic for posts, it's time to create a way for users to interact with each other by adding comments. Add the click event to the comments link for the posts with the following:

<li><a @click="toggleCommentModal(post)">comments {{ post.comments }}</a></li>

Pass in the post object as a parameter so you can assign the properties you need to your comment data object.

Next create a CommentModal.vue file in the components folder. Here is the basic markup:

<template>
  <div class="c-container">
    <a @click="$emit('close')">close</a>
    <p>add a comment</p>
    <form @submit.prevent>
      <textarea v-model.trim="comment"></textarea>
      <button @click="addComment()" :disabled="comment == ''" class="button">add comment</button>
    </form>
  </div>
</template>

Add the logic in your script tag. We need to import a few references from our Firebase file, reference our props, and set up the logic to create a comment in the addComment method.

<script>
import { commentsCollection, postsCollection, auth } from '@/firebase'

export default {
  props: ['post'],
  data() {
    return {
      comment: ''
    }
  },
  methods: {
    async addComment() {
      // create comment
      await commentsCollection.add({
        createdOn: new Date(),
        content: this.comment,
        postId: this.post.id,
        userId: auth.currentUser.uid,
        userName: this.$store.state.userProfile.name
      })

      // update comment count on post
      await postsCollection.doc(this.post.id).update({
        comments: parseInt(this.post.comments) + 1
      })

      // close modal
      this.$emit('close')
    }
  }
}
</script>

Be sure to increment the post's comment count; you will be able to see this in real time. On a side note, you could create a cloud function to update a post every time a new comment or like is associated to it, but that is outside the scope of this project.

Import the CommentModal component into your Dashboard.vue file.

import CommentModal from '@/components/CommentModal'

// add to vue instance
export default {
  components: {
    CommentModal
  },

Update your markup to include the CommentModal component. We use a transition component to make the show/hide animation smooth. We also want to add a click event to show/hide the modal.

<template>
  <div id="dashboard">
    <transition name="fade">
      <CommentModal v-if="showCommentModal" :post="selectedPost" @close="toggleCommentModal()"></CommentModal>
    </transition>
    // content
    // add click event to comments UI on post (v-for loop)
    <li><a @click="toggleCommentModal(post)">comments {{ post.comments }}</a></li>

Update the data object with the following: comment object and showCommentModal, like so:

data() {
  return {
    post: {
      content: ''
    },
    showCommentModal: false,
    selectedPost: {}
  }
}

Add a toggleCommentModal method that will be used to show/hide the modal and set the selectedPost object that we use as a prop to pass data to our CommentModal component.

toggleCommentModal(post) {
  this.showCommentModal = !this.showCommentModal

  // if opening modal set selectedPost, else clear
  if (this.showCommentModal) {
    this.selectedPost = post
  } else {
    this.selectedPost = {}
  }
}

Adding comments is complete!

Step 5: Add a way for users to “like” posts.

Now that users can add comments, let's go a step further and give them the ability to like a post. This needs to be configured so users will only be able to like a post once.

Add a click event to the likes link for the posts and pass in two parameters: post.id and post.likes. It will look like:

<li><a @click="likePost(post.id, post.likes)">likes {{ post.likes }}</a></li>

Remember to pass both the id and likesCount to the likePost method, those will be passed to our action.

likePost(id, likesCount) {
  this.$store.dispatch('likePost', { id, likesCount })
}

The last step is to create the likePost action within store/index.js. We want to create the likes in their own collection so you can see if a user has already liked that post. To do so, check if the “like” document exists. If not, create it. Then, update the post's “like” count just like you did with comments.

async likePost ({ commit }, post) {
  const userId = fb.auth.currentUser.uid
  const docId = `${userId}_${post.id}`

  // check if user has liked post
  const doc = await fb.likesCollection.doc(docId).get()
  if (doc.exists) { return }

  // create post
  await fb.likesCollection.doc(docId).set({
    postId: post.id,
    userId: userId
  })

  // update post likes count
  fb.postsCollection.doc(post.id).update({
    likes: post.likesCount + 1
  })
}

Step 6: View all comments for a post.

Let's create a way for users to click on a post to see the entire message and all comments associated to that post.

Add a click event to the post link, then pass it the post object so you have access to it within the modal.

<a @click="viewPost(post)">view full post</a>

Now add the post modal markup. You'll need to display the post and all comments.

<!-- full post modal -->
<transition name="fade">
  <div v-if="showPostModal" class="p-modal">
    <div class="p-container">
      <a @click="closePostModal()" class="close">close</a>
      <div class="post">
        <h5>{{ fullPost.userName }}</h5>
        <span>{{ fullPost.createdOn | formatDate }}</span>
        <p>{{ fullPost.content }}</p>
        <ul>
          <li><a>comments {{ fullPost.comments }}</a></li>
          <li><a>likes {{ fullPost.likes }}</a></li>
        </ul>
      </div>
      <div v-show="postComments.length" class="comments">
        <div v-for="comment in postComments" :key="comment.id" class="comment">
          <p>{{ comment.userName }}</p>
          <span>{{ comment.createdOn | formatDate }}</span>
          <p>{{ comment.content }}</p>
        </div>
      </div>
    </div>
  </div>
</transition>

To update data object, you need an empty fullPost object, postComments array, and showPostModal as way to toggle the post modal. Let’s import the commentsCollection firebase ref into Dashboard.vue and update the data object with new properties.

// import firebase commentsCollection ref
import { commentsCollection } from '@/firebase'

// data()
showPostModal: false,
fullPost: {},
postComments: []

Create viewPost(post) and closePostModal() methods. viewPost(post) will set the fullPost object, show the post modal, and fetch all comments associated to that post. closePostModal() will clear out the postComments array and close the post modal.

// create viewPost and closePostModal methods
async viewPost(post) {
  const docs = await commentsCollection.where('postId', '==', post.id).get()

  docs.forEach(doc => {
    let comment = doc.data()
    comment.id = doc.id
    this.postComments.push(comment)
  })

  this.fullPost = post
  this.showPostModal = true
},
closePostModal() {
  this.postComments = []
  this.showPostModal = false
}

That might've gotten a little intense, but now you're all set with your project's dashboard! Let's move on to the final section in this tutorial, creating project settings.

Making Vuegram's Settings

In this section, you'll create Vuegram's settings while learning how to:

  • Dispatch actions with a payload from a component.
  • Change and update data across all existing data.
  • Decouple logic from a component and execute from the store/index.js file.

These settings will allow users to update their profile information, including name and title. When they update their info, all posts and comments created by the current user will update across the board.

For this part of the tutorial, follow along with the settings.vue file in the Vuegram GitHub repo.

Step 1: Set up the markup in Settings.vue.

Let’s create the Settings.vue file in our views folder and add all the markup, directives, and methods. First we start with the markup.

<template>
  <section id="settings">
    <div class="col1">
      <h3>Settings</h3>
      <p>Update your profile</p>

      <transition name="fade">
        <p v-if="showSuccess" class="success">profile updated</p>
      </transition>

      <form @submit.prevent>
        <label for="name">Name</label>
        <input v-model.trim="name" type="text" :placeholder="userProfile.name" id="name" />

        <label for="title">Job Title</label>
        <input v-model.trim="title" type="text" :placeholder="userProfile.title" id="title" />

        <button @click="updateProfile()" class="button">Update Profile</button>
      </form>
    </div>
  </section>
</template>

Step 2: Set up the basic data.

Import our mapState so we can reference userProfile and set up our basic data structure for our settings file.

<script>
import { mapState } from 'vuex'

export default {
  data() {
    return {
      name: '',
      title: '',
      showSuccess: false
    }
  },
  computed: {
    ...mapState(['userProfile'])
  },
  methods: {
    updateProfile() {
      // logic goes here
    }
  }
}
</script>

Step 3: Create the updateProfile method.

You could add the logic here, but we want to show you how to pass data to an action method in the store/index.js file from a component method. This can be helpful when you're building a large app and want to abstract the logic for use in multiple components. The method we use in these next few sections, updateProfile(), could be called from any number of views. If you added the functionality, that is.

First you need to dispatch an action to update the settings. When you dispatch the action, you'll pass along a payload, which is just an object that contains data. You then clear out these fields, show a success message, and hide it after two seconds.

updateProfile() {
  this.$store.dispatch('updateProfile', {
    name: this.name !== '' ? this.name : this.userProfile.name,
    title: this.title !== '' ? this.title : this.userProfile.title
  })

  this.name = ''
  this.title = ''

  this.showSuccess = true

  setTimeout(() => {
    this.showSuccess = false
  }, 2000)
}

Step 4: Create the action method updateProfile in the store/index.js file.

For action methods, the first parameter is an object that allows us to use commit and state unprefixed. The second parameter is the data payload. You will create two variables, name and title, from the payload that you passed in the settings method. You then need to query for all posts and comments from the current user and update the userName values.

async updateProfile({ dispatch }, user) {
  const userId = fb.auth.currentUser.uid
  // update user object
  const userRef = await fb.usersCollection.doc(userId).update({
    name: user.name,
    title: user.title
  })

  dispatch('fetchUserProfile', { uid: userId })

  // update all posts by user
  const postDocs = await fb.postsCollection.where('userId', '==', userId).get()
  postDocs.forEach(doc => {
    fb.postsCollection.doc(doc.id).update({
      userName: user.name
    })
  })

  // update all comments by user
  const commentDocs = await fb.commentsCollection.where('userId', '==', userId).get()
  commentDocs.forEach(doc => {
    fb.commentsCollection.doc(doc.id).update({
      userName: user.name
    })
  })
}

And that's it! Navigate back to the dashboard and you'll see the changes have been reflected across the board for user profiles, posts, and comments. If something seems off, make sure you referenced the local data in your payload object correctly when dispatching an action. Otherwise, congratulations! You've successfully created your very own Vuegram social media app using Vue.js and Firebase.

Concluding Note

Now that you've built this project, you should have a firm understanding of how Vue.js is used in a real-world scenario. For additional practice, try implementing more features and building on the existing structure. With your newfound knowledge, you can add features such as:

  • Adding comments to a post when viewing the full post
  • Allowing a user to edit or delete their comments and posts
  • Creating additional userProfile information
  • Letting users add other users as friends
  • Adding better error handling using try/catch for all requests

We want to know how you use this resource in your own apps. Access the Vuegram project at any time on GitHub. Keep reading our blog and keep in touch on Twitter and Facebook for updates, including announcements when we release additional educational resources and open source projects. Happy Vue.js-ing!

Written By:

Alec Plummer

Alec is a web developer who enjoys all things JavaScript. When he’s not writing code, you can find him exploring the depths of VR or an epic space opera.