Vue + Routes + Firebase auth - Sync problem


#1

Hello guys, sorry for my English, but I’m kinda stuck xD (I will try to provide as much detail as possible)

I’m building a simple app to learn the firebase + vue basics with 2 features:

  • Basic auth
  • Basic item list from the user

My problem is: whenever I go to the list page (logged in), my user takes time to authenticate, then the response from firebase is Cannot read property ‘ref’ of null

How can I get around this problem?


I have the following project structure:

src
|
-- main.js     -> main file where I init all my files
-- routes.js   -> main routes files
-- database.js -> basic firebase database operations
-- configs.js  -> firebase and application config variables
-- auth.js     -> fire auth operations 
-- App.vue     -> Main application container
-- components
   |
   |
   -- Hello.vue -> My item list component
// Hello.vue
<template>
  <div class="hello">
    <h1>List</h1>
    {{ items }}
  </div>
</template>

<script>
import database from '../database'

export default {
  name: 'hello',
  firebase: { --> Im using vuefire
    classes: database.getDb().ref('/classes') --> Here, when I do this request, i get the error
  }
}
</script>

The rest of the files

// main.js
// imports...

const initApp = () => {
  firebase.initializeApp(firebaseConfig)
  database.init()
  auth.init()
}

initApp()

/* eslint-disable no-new */
new Vue({
  router,
  ...App
}).$mount('#app')
// routes.js
// imports ...

const routes = [
  { path: '/items', component: Hello },
  { path: '/', redirect: 'items' }
]

export default new VueRouter({
  routes
})
// database.js
// imports...

let database = null

const init = function () {
  database = firebase.database()
}

const getRef = function (path) {
  return database.ref(path)
}

const getDb = function () {
  return database
}

export default { init, getRef, getDb }
// config.js
let firebaseConfig = {...}

export { firebaseConfig }
// auth.js
// imports...

const user = {
  displayName: '',
  email: '',
  emailVerified: false,
  photoURL: '',
  uid: ''
}

const initAuthUI = () => {
  // FirebaseUI config.
  const uiConfig = {
    signInSuccessUrl: '',
    signInOptions: [
      // Leave the lines as is for the providers you want to offer your users.
      firebase.auth.GoogleAuthProvider.PROVIDER_ID,
      firebase.auth.EmailAuthProvider.PROVIDER_ID
    ],
    // Terms of service url.
    tosUrl: '/'
  }

  // Initialize the FirebaseUI Widget using Firebase.
  const ui = new firebaseui.auth.AuthUI(firebase.auth())
  // The start method will wait until the DOM is loaded.
  ui.start('#firebaseui-auth-container', uiConfig)
}

const init = () => {
  firebase.auth().onAuthStateChanged((theUser) => {
    if (theUser) {
      user.displayName = theUser.displayName
      user.email = theUser.email
      user.emailVerified = theUser.emailVerified
      user.photoURL = theUser.photoURL
      user.uid = theUser.uid

    // Object.assign(user, theUser)
    // console.log(user.uid)
    } else {
      // User is signed out.
      // console.log('signed out')
      user.displayName = ''
      user.email = ''
      user.emailVerified = false
      user.photoURL = ''
      user.uid = ''
      initAuthUI()
    }
  }, (error) => {
    console.log(error)
  })
}

const getUser = () => {
  return user
}

const logout = () => {
  firebase.auth().signOut().then(() => {
    // Sign-out successful.
    window.location.reload()
  }, (error) => {
    // An error happened.
    console.log(error)
    window.location.reload()
  })
}

export default { init, getUser, logout }
// App.vue
<template>
  <div id="app">
    <router-link to="/items">Go to Foo</router-link>
    <button v-show="user.uid" @click="logout">
      Logout
    </button>

    <div class="login" v-show="!user.uid">
        <div id="firebaseui-auth-container"></div>
    </div>
    <router-view v-show="user.uid"></router-view>
  </div>
</template>

<script>
import auth from './auth'

const user = auth.getUser()

export default {
  name: 'app',
  data: function () {
    return {
      user
    }
  },
  methods: {
    logout: auth.logout
  }
}
</script>

Thanks in advance :grin:


#2

I think I’ve been able to solve the problem :wink:

Solution: Use the beforeEach of the Navigation Guard and prevent the user from going to the next route until the authentication function is complete.
Then just request the auth user on the created lifecycle hoock and send your firebase request

Below is the complete code.
I hope you can help someone, or that someone can give me a better suggestion :grin:


// main.js
import Vue from 'vue'
import App from './App'
import router from './routes'
import firebase from 'firebase'
import VueFire from 'vuefire'
import database from './database'
import { firebaseConfig } from './configs'

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-default/index.css'
import 'normalize.css'

Vue.use(ElementUI)
Vue.use(VueFire)

const initApp = () => {
  firebase.initializeApp(firebaseConfig)
  database.init()
}

initApp()

/* eslint-disable no-new */
new Vue({
  router,
  ...App
}).$mount('#app')
// routes.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import auth from './auth'

Vue.use(VueRouter)

import Hello from './components/Hello'

const routes = [
  { path: '/classes', component: Hello },
  { path: '/', redirect: 'classes' }
]

const router = new VueRouter({
  routes
})

router.beforeEach((to, from, next) => {
  auth.authUser().then(() => {
    next()
  })
})

export default router
// auth.js
import firebase from 'firebase'
import firebaseUiAuthCss from 'firebaseui/dist/firebaseui.css' /* eslint no-unused-vars: 0 */
import firebaseui from 'firebaseui' /* eslint no-unused-vars: 0 */

const user = {
  displayName: '',
  email: '',
  emailVerified: false,
  photoURL: '',
  uid: ''
}

const getUser = () => {
  return user
}

const authUser = () => {
  return new Promise((resolve, reject) => {
    firebase.auth().onAuthStateChanged((theUser) => {
      _checkUser(theUser)
      resolve(theUser)
    }, (error) => {
      console.log(error)
    })
  })
}

const logout = () => {
  firebase.auth().signOut().then(() => {
    // Sign-out successful.
    window.location.reload()
  }, (error) => {
    // An error happened.
    console.log(error)
    window.location.reload()
  })
}

const _checkUser = (theUser) => {
  if (theUser) {
    user.displayName = theUser.displayName
    user.email = theUser.email
    user.emailVerified = theUser.emailVerified
    user.photoURL = theUser.photoURL
    user.uid = theUser.uid
  } else {
    user.displayName = ''
    user.email = ''
    user.emailVerified = false
    user.photoURL = ''
    user.uid = ''
    _initAuthUI()
  }
}

const _initAuthUI = () => {
  const uiConfig = {
    signInOptions: [
      // Leave the lines as is for the providers you want to offer your users.
      firebase.auth.GoogleAuthProvider.PROVIDER_ID,
      firebase.auth.EmailAuthProvider.PROVIDER_ID
    ],
    // Terms of service url.
    tosUrl: '/'
  }

  // Initialize the FirebaseUI Widget using Firebase.
  const ui = new firebaseui.auth.AuthUI(firebase.auth())
  // The start method will wait until the DOM is loaded.
  ui.start('#firebaseui-auth-container', uiConfig)
}

export default { getUser, logout, authUser }
// database.js
import firebase from 'firebase'

let database = null

const init = function () {
  database = firebase.database()
}

const getRef = function (path) {
  return database.ref(path)
}

const getDb = function () {
  return database
}

export default { init, getRef, getDb }
//config.js
let firebaseConfig = {
  apiKey: '...',
  authDomain: '...',
  databaseURL: '...',
  storageBucket: '...',
  messagingSenderId: '...'
}

export { firebaseConfig }
// App.vue
<template>
  <div id="app">
    <router-link to="/items">Go to Foo</router-link>
    <button v-show="user.uid" @click="logout">
      Logout
    </button>

    <div class="login" v-show="!user.uid">
        <div id="firebaseui-auth-container"></div>
    </div>
    <router-view v-show="user.uid"></router-view>
  </div>
</template>

<script>
import auth from './auth'

const user = auth.getUser()

export default {
  name: 'app',
  data: function () {
    return {
      user
    }
  },
  methods: {
    logout: auth.logout
  }
}
</script>
// Hello.vue
<template>
  <div class="hello">
    {{ items }}
    <ul>
      <li v-for="item in items">
        {{ item['.value'] }}
      </li>
    </ul>
  </div>
</template>

<script>
import auth from '../auth'
import database from '../database'

// Using vuefire
const getItems = function () {
  const user = auth.getUser()
  if (user.uid) {
    this.$bindAsArray('classes', database.getRef(`items/${user.uid}`))
  }
}

// Without vuefire
const getItems = function () {
  const user = auth.getUser()
  if (user.uid) {
    database.getRef(`items/${user.uid}`).orderByValue().limitToLast(3).on('value', function (snapshot) {
      snapshot.forEach(function(data) {
        console.log("The " + data.key + " score is " + data.val())
      })
    })
  }
}

export default {
  name: 'hello',
  data: () => {
    return {
      items: []
    }
  },
  created: getItems
}
</script>

#3

Hello,

I like how you have architectured your solution, it is easy to follow and understand.

I however keep getting the error below even after database been initialised in main.js;

Uncaught TypeError: Cannot read property 'ref' of null

Any idea what could be causing this?


#4

I was finally able to find my way around the problem.

I was declaring my database variable globally. I made it local and everything is okay now.

Thanks


#5

Awesome solution !! Love it.