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

Development Web

Developers are adopting Vue.js at a rapid pace. Boasting over 92,000 stars on GitHub, the modern, modular JavaScript library is quickly becoming a compelling option for web app development. This adoption is in part because Vue.js is so easy to integrate into an existing project. It's fast. It's extremely powerful. And you can build a dynamic, scalable, and maintainable Single-Page Application from scratch with it.

Savvy Apps is one of the earlier app agencies to adopt Vue.js development, using it to power web apps like PetDoc and Pocket Prep. We decided to leverage our experience to help teach developers who are just getting started with Vue.js. Since we believe the best way to learn new concepts is by trying them out in real-world scenarios, we 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 get hands-on experience creating a production-ready app using Vue.js.

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 a user having to reload the browser. The growing popularity of Vue.js cannot be understated. We prefer it because it's lightweight, modular, and requires 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 a major company or organization. While that type of backing comes with its own positives, we feel like Vue.js works better as an open-source, third-party framework as it potentially carries fewer motives or biases. Normally, we would be concerned about the longevity of a product without that type of support. We believe Vue is here to stay, however, thanks to its popularity and the activity around it so far.

We won't dive too deep into the benefits of using Vue.js as there are already a number of solid resources that go in-depth on this very topic. You should check out Vue.js' own comparison of itself with other frameworks to get a feel for the advantages/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 features as well as its development roadmap on the recently launched 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 and 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 and can serve as a foundation to build more complex and scalable web apps. They'll give you more of an idea of how you'll want your Vue app to scale and give you the ability to manage many files easily. You'll also be positioned to prevent pitfalls before they happen, potentially saving a significant amount of development time.

Head over to GitHub to access the Vuegram project and use it to follow along with the rest of this tutorial. You can use the table of contents links in the right side of this page to skip to the section of your choice. With the housekeeping out of the way, let's get started!

Initial Setup With Vue CLI

You may be more familiar with something similar to jQuery where you just download a single file, add it to the project, and reference it in the main index.html file. Setting up a production-ready project with Vue is a bit more involved than that.

This section is dedicated to explaining how to set up your completed, production-ready app using Vue CLI. We recommend using Vue CLI to complete this setup as it's the official command line tool for quickly scaffolding Vue.js projects. While there are other ways to scaffold a project, Vue CLI is maintained by the core Vue development team.

Note that this tutorial demonstrates how to set up your project for the world to use; you'll build and compile everything down into a single, deployable folder. If you just want to explore this project locally or drop it into an existing project, use the basic setup instructions included in GitHub.

Step 1: Initialize your project with Vue CLI.

vue init webpack vue-app

Just follow the steps when prompted. Be sure to choose runtime + compiler (it's the default choice). Select “yes” to vue-router. For the purpose of this article, select “no” to ESlint, unit tests, and End-to-End (E2E) tests. Choose your package manager. We will use npm for this project.

Step 2: Install dependencies.

First, install Vuex. This will be important for state management.

npm i vuex

Then install sass-loaders; they will handle the transpiling of SCSS for this project.

npm i node-sass sass-loader --save-dev

Step 3: Run the starter project.

npm run dev

Open a browser and go to http://localhost:8080/. Add the Vue Devtools extension to your browser if you haven't already.

Step 4: Remove starter files and clean up the project.

Optional: Update your .editorconfig file if you want to change the formatting. This is purely personal preference. We prefer tabs over spaces.

...
indent_style = tab
indent_size = 4
...

Remove the starter files, though make sure to keep the components folder (src/components). Clean up the App.vue file by removing the Vue.js logo image and style. Your cleaned up code should look like:

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

<script>
    export default {
        
    }
</script>

Step 5: Create all of the files you'll need for this project.

Build your views under the components folder with placeholder names. You'll also want to create a basic template so you can copy it into other views.

<template>
    <div>
        
    </div>
</template>

<script>
    export default {
        
    }
</script>

Here's a quick breakdown of the files you will use in this project:

  • Login.vue: The default view if a user isn't authenticated. Here a user will encounter log-in, sign-up, and forgot-password scenarios.
  • Dashboard.vue: Where the user will create and read posts as well as interact with posts by commenting or liking them.
  • Navigation.vue: This gives users a way to navigate between the different views in this project.
  • Settings.vue: Where users can update their profiles.

Now that you've completed the initial setup, you can move on to handling the app's state management.

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 of the 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 just 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 would be 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 the data reliable as it provides a single point of truth. Whereas with props you could 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 to learn that we'll use Vuex for managing the entirety of this app's local state. There's only one main step here to tackle, so let's get started.

Step 1: Create the basic structure for your store.js file.

Your store.js file will handle all of your app's local state. Import the dependencies and scaffold the empty Vuex store object.

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export const store = new Vuex.Store({
    state: {
        
    },
    actions: {
        
    }, 
    mutations: {
        
    }
})

Update the main.js file to import store.js, then update the Vue instance by adding the store object. Finally, update the render function.

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

Vue.config.productionTip = false

new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App)
})

With this complete, you're ready to move on to setting up the app's Firebase database.

Setting Up a Database With Firebase Cloud Firestore

We're big fans of Firebase. Savvy Apps 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 firebaseConfig.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 firebaseConfig.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 firebaseConfig.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 firebaseConfig.js file with your own Firebase credentials.

Since Cloud Firestore is in beta, changes will happen from time to time. As of the publication of this article, you need to update your firebaseConfig.js file to include some code below your Firebase utilities.

const settings = { timestampsInSnapshots: true }
db.settings(settings)

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

import firebase from 'firebase'
import 'firebase/firestore'

// firebase init goes here
const config = {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: ""
}
firebase.initializeApp(config)

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

// date issue fix according to firebase
const settings = {
    timestampsInSnapshots: true
}
db.settings(settings)

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

export {
    db,
    auth,
    currentUser,
    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 will make sure Firebase initializes before loading the app when a user refreshes a page.

import Vue from 'vue'
import App from './App'
import router from './router'
import { store } from './store.js'
const fb = require('./firebaseConfig.js')

Vue.config.productionTip = false

// handle page reloads
let app
fb.auth.onAuthStateChanged(user => {
    if (!app) {
        app = new Vue({
            el: '#app',
            router,
            store,
            render: h => h(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 lets you define how users interact and access the different views in your app. Since Vue can create Single-Page Applications, there's no need to reload your app when switching between views. You handle this 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 log-in 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 and all views.

This is just your standard import for JavaScript. As a side note, you can reference all of your views like this:

import ViewName from '@/views/ViewName

There's no need to include the .vue extension.

Step 2: Prepare for navigation guards.

Navigation guards give you more control over how users interact with your routes by allowing you to add logic to your routes. Use navigation guards to check if the route a user is navigating to requires authentication. If it does, you can check if the user is authenticated before moving on. If not, you can redirect the user to a different route or just cancel the request and alert the user.

To prepare for navigation guards you first need to update the way you export the router object.

const router = new Router({})

Step 3: Set mode to history.

History mode allows us to use more readable url paths (/dashboard) as opposed to the default option (/#/dashboard).

mode: 'history'

Step 4: Set up all views.

Create a catchall route to handle incorrect routes.

{ path: '*', redirect: '/dashboard' }

Next create your basic routes and enable authentication with the meta object. The log-in route, for example, is where users will log in, register, and start the forgot password process. Note that the Dashboard and Settings routes need the requiresAuth meta tag. These meta tags allow us to authenticate each route.

import Vue from 'vue'
import Router from 'vue-router'
import firebase from 'firebase'

import Login from '@/components/Login'
import Dashboard from '@/components/Dashboard'
import Settings from '@/components/Settings'

Vue.use(Router)

const router = new Router({
    mode: 'history',
    routes: [
        {
            path: '*',
            redirect: '/dashboard'
        },
        {
            path: '/login',
            name: 'Login',
            component: Login
        },
        {
            path: '/dashboard',
            name: 'Dashboard',
            component: Dashboard,
            meta: {
                requiresAuth: true
            }
        },
        {
            path: '/settings',
            name: 'Settings',
            component: Settings,
            meta: {
                requiresAuth: true
            }
        }
    ]
})

export default router

Step 5: Test routes manually.

You'll want to test /, /login, /dashboard, and /settings.

Step 6: Set up the beforeEach navigation guard.

Check if the route exists and requires authentication. Then 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 log-in view. If the route has the requiresAuth meta property set to true and and the user is logged in, send them to whatever route they are trying to visit. For else, you just send them to whatever 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)
    const currentUser = firebase.auth().currentUser

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

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

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

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

With this complete, let's move on to creating the app's log-in view.

Creating the Log-in and Sign-up View

Update [6/27/2018]: Due to updates in the Firebase API, you may need to access your return objects differently than what this resource originally recommended for your login and signup methods. You'll find more information about this in Step 3 of this section.

In this section you'll not only create a functioning log-in/sign-up view for your app, you'll learn some basic concepts of Vue.js and Vuex, and get up and running with single component files. The concepts introduced in this section are as follows.

For Vue.js:

For Vuex:

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

A quick note: we keep all logic for the log-in authentication in this view to show the different patterns. For settings you will abstract all logic to store.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 './assets/scss/app.scss'
import Vue from 'vue'
import App from './App'
import router from './router'
import { store } from './store.js'
const fb = require('./firebaseConfig.js')
import './assets/scss/app.scss'

Vue.config.productionTip = false

// handle page reloads
let app
fb.auth.onAuthStateChanged(user => {
    if (!app) {
        app = new Vue({
            el: '#app',
            router,
            store,
            render: h => h(App)
        })
    }
})

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>

                    <label for="email1">Email</label>
                    <input type="text" placeholder="[email protected]" id="email1" />

                    <label for="password1">Password</label>
                    <input type="password" placeholder="******" id="password1" />

                    <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 {
    
    }
</script>

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 know 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>

                    <label for="email1">Email</label>
                    <input v-model.trim="loginForm.email" type="text" placeholder="[email protected]" id="email1" />

                    <label for="password1">Password</label>
                    <input v-model.trim="loginForm.password" type="password" placeholder="******" id="password1" />

                    <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 log-in logic.

For the log-in method, import the Firebase utilities inside the script tag above the export default {} statement.

const fb = require('../firebaseConfig.js')

Go ahead and create the log-in method:

login() {
    fb.auth.signInWithEmailAndPassword(this.loginForm.email, this.loginForm.password).then(user => {
        this.$store.commit('setCurrentUser', user)
        this.$store.dispatch('fetchUserProfile')
        this.$router.push('/dashboard')
    }).catch(err => {
        console.log(err)
    })
}

If the above doesn't work, you may need to access your return objects differently for your login and signup methods. This is due to updates in the Firebase API.

// Firebase has changed the return object, you should pass in user.user now
this.$store.commit('setCurrentUser', user.user) 
// instead of 
this.$store.commit('setCurrentUser', user) 

The same applies to the sign up method.

Now update the template's log-in button with a click event.

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

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 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 firebaseConfig file.

const fb = require('./firebaseConfig.js')

Add the properties to the state object.

state: {
    currentUser: null,
    userProfile: {}
}

Set up the fetchUserProfile action method.

actions: {
    fetchUserProfile({ commit, state }) {
        fb.usersCollection.doc(state.currentUser.uid).get().then(res => {
            commit('setUserProfile', res.data())
        }).catch(err => {
            console.log(err)
        })
    }
}

Create the setCurrentUser and setUserProfile mutations to update the user in the state object.

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

Your finished code should look like:

Step 4: Create the sign-up logic.

To add the signup, you first need to add your markup and data binding to the sign-up form inputs like you did for the log-in form.

<form @submit.prevent>
    <h1>Get Started</h1>

    <label for="name">Name</label>
    <input v-model.trim="signupForm.name" type="text" placeholder="Savvy Apps" id="name" />

    <label for="title">Title</label>
    <input v-model.trim="signupForm.title" type="text" placeholder="Company" id="title" />

    <label for="email2">Email</label>
    <input v-model.trim="signupForm.email" type="text" placeholder="[email protected]" id="email2" />

    <label for="password2">Password</label>
    <input v-model.trim="signupForm.password" type="password" placeholder="min 6 characters" id="password2" />

    <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.

data() {
    return {
        loginForm: {
            email: '',
            password: ''
        },
        signupForm: {
            name: '',
            title: '',
            email: '',
            password: ''
        }
    }
}

Step 5: Create the sign-up method.

The sign-up 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.

signup() {
    fb.auth.createUserWithEmailAndPassword(this.signupForm.email, this.signupForm.password).then(user => {
        this.$store.commit('setCurrentUser', user)

        // create user obj
        fb.usersCollection.doc(user.uid).set({
            name: this.signupForm.name,
            title: this.signupForm.title
        }).then(() => {
            this.$store.dispatch('fetchUserProfile')
            this.$router.push('/dashboard')
        }).catch(err => {
            console.log(err)
        })
    }).catch(err => {
        console.log(err)
    })
}

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 screenshot below will show you where to add this.

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

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="col2" :class="{ 'signup-form': !showLoginForm }">

Go ahead and test your signup and login. They work (hopefully)!

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

Next you need to create a way to show users that an action is being performed when they log in or sign up. Add a property to the data object called performingRequest, and toggle between true and false to show or hide the loading animation while the data is fetched. You can use the wrapper <transition name=”fade”> to add animations when showing/hiding parts of the markup.

<transition name="fade">
    <div v-if="performingRequest" class="loading">
        <p>Loading...</p>
    </div>
</transition>

You'll add this new markup here:

Now update the log-in and sign-up methods to include the performingRequest property. Set it to true before making the request. This lets the users know something is happening once they log in or sign up to use the app. You'll want to hide it by setting it to false after you have the data you need or when an error occurs. Check out the screenshot below to see where to add the performingRequest properties.

You'll also want to add some sort of error handling to let users know when an action has failed, like if they entered a wrong username/password combination. Add the error at the bottom of the form. Your markup should look like this:

<transition name="fade">
    <div v-if="errorMsg !== ''" class="error-msg">
        <p>{{ errorMsg }}</p>
    </div>
</transition>

The following screenshot shows where to add the errorMsg markup.

Add the errorMsg property to the object and set the value to an empty string. Next, update the errorMsgproperty in the catch statement to show users that there is an error while logging in. Lastly, update the toggleForm method to clear out any errors when switching forms. Note where the toggleForm method is located:

Step 6: Create forgot password markup and logic.

You'll want to update the markup and add a click event to the forgot password link. Optional: you can edit the password reset email template through the Firebase console.

<a @click="togglePasswordReset">Forgot Password</a>

Use the following image to see where you should add the errorMsg properties:

Then update the form conditionals to hide/show the correct forms, and update the dynamic class:

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

The screenshot below illustrates where to add the dynamic class binding.

More updating! You'll want to update the data object to add a password form and show/success booleans, like so:

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.

resetPassword() {
    this.performingRequest = true

    fb.auth.sendPasswordResetEmail(this.passwordForm.email).then(() => {
        this.performingRequest = false
        this.passwordResetSuccess = true
        this.passwordForm.email = ''
    }).catch(err => {
        console.log(err)
        this.performingRequest = false
        this.errorMsg = err.message
    })
}

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

Step 7: 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 of the views. Please note that in this case you just 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 navigation.vue file in the Vuegram GitHub repo.

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

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

<template>
    <header>
        <section>
            <div class="col1">
                <router-link to="dashboard"><h3>Vuegram</h3></router-link>
                <ul class="inline">
                    <li><router-link to="dashboard">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>

Step 2: Create the log-out method.

Let's create a method to log users out called logout. You will also create an action method named clearData in the store.js file to clear out the current user's data on logout.

<script>
    const fb = require('../firebaseConfig.js')

    export default {
        methods: {
            logout() {
                fb.auth.signOut().then(() => {
                    this.$store.dispatch('clearData')
                    this.$router.push('/login')
                }).catch(err => {
                    console.log(err)
                })
            }
        }
    }
</script>

You'll add that here:

Your completed Navigation.vue code should look like:

Step 3: Update App.vue.

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

...mapState(['currentUser'])

Set up app.vue to use the navigation component.

components: { Navigation }

Then conditionally show the navigation based on the user's authentication.

<Navigation v-if="currentUser"></Navigation>

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 you tell your component to use a child component:

components: { yourChildComponent }

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 a user out if for some reason they reload 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 store.js.

The only thing you need to do to fix this is to update the store.js file to dispatch and commit actions and mutations 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:

// handle page reload
fb.auth.onAuthStateChanged(user => {
    if (user) {
        store.commit('setCurrentUser', user)
        store.dispatch('fetchUserProfile')
    }
})

Now log in and reload the page. Your navigation should appear as intended.

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>{{ userProfile.name }}</h5>
                    <p>{{ userProfile.title }}</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'
    const fb = require('../firebaseConfig.js')
    
    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:

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

const fb = require('../firebaseConfig.js')

export default {
    data() {
        return {
            post: {
                content: ''
            }
        }
    },
    computed: {
        ...mapState(['userProfile'])
    },
    methods: {
        createPost() {
            fb.postsCollection.add({
                createdOn: new Date(),
                content: this.post.content,
                userId: this.currentUser.uid,
                userName: this.userProfile.name,
                comments: 0,
                likes: 0
            }).then(ref => {
                this.post.content = ''
            }).catch(err => {
                console.log(err)
            })
        }   
    }
}

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.

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

fb.postsCollection.orderBy('createdOn', 'desc').onSnapshot(querySnapshot => {})

Use orderBy() to prompt the posts to come back newest first. Then 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:

store.commit('setPosts', postsArray)
// handle page reload
fb.auth.onAuthStateChanged(user => {
    if (user) {
        store.commit('setCurrentUser', user)
        store.dispatch('fetchUserProfile')

        // realtime updates from our posts collection
        fb.postsCollection.orderBy('createdOn', 'desc').onSnapshot(querySnapshot => {
            let postsArray = []

            querySnapshot.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 } and update the clearData() method to clear posts on user logout with commit('setPosts', null).

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', 'currentUser', 'posts'])

Then 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" class="post">
        <h5>{{ post.userName }}</h5>
        <span>{{ post.createdOn | formatDate }}</span>
        <p>{{ post.content | trimLength }}</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, you need to leverage the power of custom filters within Vue. First, install moment using npm or yarn.

npm i moment

Then import moment into the dashboard component below where we require our firebaseUtils file.

import moment from 'moment'

Then create your filters. You 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 you will 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 }}

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

Step 4: Show/hide posts based on when they come in.

Since Vuegram is real-time, you need a way to stop new posts from flooding the dashboard, causing the user to lose their place and the DOM to jump around on them. To do this, create a new array in the state (hiddenPosts: []) and send all new posts to that array after the initial data load. Then show an indicator that new posts have been created. A user will see that indicator, click, and the posts will re-render to display all current posts.

To hide posts as they come in unless created by current user, you need to update the snapshot query in the store.js file. First, check to see if the post is by the currentUser. If yes, execute as before. The current user posts should always show up as soon as they're published. If not, you need to make sure the data has already been loaded and these are new posts and not updates to existing posts (for example, comments or likes).

// handle page reload
fb.auth.onAuthStateChanged(user => {
    if (user) {
        store.commit('setCurrentUser', user)
        store.dispatch('fetchUserProfile')

        // realtime updates from our posts collection
        fb.postsCollection.orderBy('createdOn', 'desc').onSnapshot(querySnapshot => {
            // check if created by currentUser
            let createdByCurrentUser
            if (querySnapshot.docs.length) {
                createdByCurrentUser = store.state.currentUser.uid == querySnapshot.docChanges[0].doc.data().userId ? true : false
            }

            // add new posts to hiddenPosts array after initial load
            if (querySnapshot.docChanges.length !== querySnapshot.docs.length
                && querySnapshot.docChanges[0].type == 'added' && !createdByCurrentUser) {

                let post = querySnapshot.docChanges[0].doc.data()
                post.id = querySnapshot.docChanges[0].doc.id

                store.commit('setHiddenPosts', post)
            } else {
                let postsArray = []

                querySnapshot.forEach(doc => {
                    let post = doc.data()
                    post.id = doc.id
                    postsArray.push(post)
                })

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

Add the empty hiddenPosts array to the state (hiddenPosts: []). Update the mutations, then update the setPosts method to handle clearing data like setHiddenPosts.

Now you need a way to show the hidden posts. For this we add some markup and create a showNewPosts method in the dashboard component. To update the markup, add this code above the posts container:

<transition name="fade">
    <div v-if="hiddenPosts.length" @click="showNewPosts" class="hidden-posts">
        <p>
            Click to show <span class="new-posts">{{ hiddenPosts.length }}</span> 
            new <span v-if="hiddenPosts.length > 1">posts</span><span v-else>post</span>
        </p>
    </div>
</transition>

Include hiddenPosts in the mapState .

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

Create the showNewPosts method. We need to clear our hiddenPosts array and update the existing posts array to re-render the DOM.

And with that you're all done with the 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 5: 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:

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

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

Next add modal markup, so when a user clicks on the comments link it will give them a place to add a comment.

<!-- comment modal -->
<transition name="fade">
    <div v-if="showCommentModal" class="c-modal">
        <div class="c-container">
            <a @click="closeCommentModal">X</a>
            <p>add a comment</p>
            <form @submit.prevent>
                <textarea v-model.trim="comment.content"></textarea>
                <button @click="addComment" :disabled="comment.content == ''" class="button">add comment</button>
            </form>
        </div>
    </div>
</transition>

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

data() {
    return {
        post: {
            content: ''
        },
        comment: {
            postId: '',
            userId: '',
            content: '',
            postComments: 0
        },
        showCommentModal: false
    }
}

Add methods to create a comment. Use openCommentModal() to set the comment properties and show the comment modal. closeCommentModal() will clear the comment properties and hide the comment modal. addComment() will create a new comment and associate to a specific post.

openCommentModal(post) {
    this.comment.postId = post.id
    this.comment.userId = post.userId
    this.comment.postComments = post.comments
    this.showCommentModal = true
},
closeCommentModal() {
    this.comment.postId = ''
    this.comment.userId = ''
    this.comment.content = ''
    this.showCommentModal = false
},
addComment() {
    let postId = this.comment.postId
    let postComments = this.comment.postComments
    
    fb.commentsCollection.add({
        createdOn: new Date(),
        content: this.comment.content,
        postId: postId,
        userId: this.currentUser.uid,
        userName: this.userProfile.name
    }).then(doc => {
        fb.postsCollection.doc(postId).update({
            comments: postComments + 1
        }).then(() => {
            this.closeCommentModal()
        })
    }).catch(err => {
        console.log(err)
    })
}

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. Adding comments complete!

Step 6: 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. You need to configure this 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 so:

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

Keep in mind that when you create a likePost method, you 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.

likePost(postId, postLikes) {
    let docId = `${this.currentUser.uid}_${postId}`

    fb.likesCollection.doc(docId).get().then(doc => {
        if (doc.exists) {
            return
        }

        fb.likesCollection.doc(docId).set({
            postId: postId,
            userId: this.currentUser.uid
        }).then(() => {
            // update post likes
            fb.postsCollection.doc(postId).update({
                likes: postLikes + 1
            })
        })
    }).catch(err => {
        console.log(err)
    })
}

Step 7: 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.

<!-- post modal -->
<transition name="fade">
    <div v-if="showPostModal" class="p-modal">
        <div class="p-container">
            <a @click="closePostModal" class="close">X</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" 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.

data() {
    return {
        post: {
            content: ''
        },
        comment: {
            postId: '',
            userId: '',
            content: '',
            postComments: 0
        },
        showCommentModal: false,
        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.

viewPost(post) {
    fb.commentsCollection.where('postId', '==', post.id).get().then(docs => {
        let commentsArray = []

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

        this.postComments = commentsArray
        this.fullPost = post
        this.showPostModal = true
    }).catch(err => {
        console.log(err)
    })
},
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 the 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.js file.

These settings will allow the user 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.

Lets add all the markup, directives and methods.

<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 mapState and include userProfile.

import { mapState } from 'vuex'

export default {
    data() {
        return {
        
        }
    },
    computed: {
        ...mapState(['userProfile'])
    },
    methods: {
        
    }
}

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.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.

this.$store.dispatch('updateProfile', { name, title })

Then you clear out these fields, show a success message, and hide it after two seconds.

import { mapState } from 'vuex'

export default {
    data() {
        return {
            name: '',
            title: '',
            showSuccess: false
        }
    },
    computed: {
        ...mapState(['userProfile'])
    },
    methods: {
        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.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. Then you need to query for all posts and comments from the current user and update the userName values.

updateProfile({ commit, state }, data) {
    let name = data.name
    let title = data.title

    fb.usersCollection.doc(state.currentUser.uid).update({ name, title }).then(user => {
        // update all posts by user to reflect new name
        fb.postsCollection.where('userId', '==', state.currentUser.uid).get().then(docs => {
            docs.forEach(doc => {
                fb.postsCollection.doc(doc.id).update({
                    userName: name
                })
            })
        })
        // update all comments by user to reflect new name
        fb.commentsCollection.where('userId', '==', state.currentUser.uid).get().then(docs => {
            docs.forEach(doc => {
                fb.commentsCollection.doc(doc.id).update({
                    userName: name
                })
            })
        })
    }).catch(err => {
        console.log(err)
    })
}

Step 5: Create a query snapshot on the userProfile collection.

We do this so it updates on the fly. Add the snapshot reference in the fb.auth.onAuthStateChanged method in store.js within our if (user) block. Then commit those changes to the state whenever they update.

fb.usersCollection.doc(user.uid).onSnapshot(doc => {
    store.commit('setUserProfile', doc.data())
})

And that's it! Navigate back to the dashboard and notice 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 you could try implementing more features and building upon the existing structure. Some features you could add with the knowledge gained from this article include:

  • 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

Let us know how you use this resource in your own apps. You can access the Vuegram project at any time on GitHub. Read our blog and be sure to keep in touch on Twitter and Facebook for updates, including announcements when we release additional educational resources and open source projects.

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.

You made it this far so...