I really liked the simplicity of using this.$bind and this.$rtdbBind this project provides. Recently, I started to use the Composition API for my projects and unfortunately, these functions do not make much sense since there is no single this.data reactive to bind firebase references to anymore.
Are you planning to add some sort of Composition API support to Vuefire anytime soon?
In the meantime, I came up with a workaround using the functions provided by vuefire-core libary and created some sort of Composition API wrapper for them. I hope it can be useful to some of you. I tried to find an existing solution, but what I found did not support both database types and made a distinction between binding documents and collections (Link).
use-firestore.js
import {bindCollection, bindDocument, walkSet,} from '@posva/vuefire-core'
const ops = {
set: (target, key, value) => walkSet(target, key, value),
add: (array, index, data) => array.splice(index, 0, data),
remove: (array, index) => array.splice(index, 1),
}
const binds = []
export function useFirestore() {
function bind(vm, key, ref, options) {
return new Promise((resolve, reject) => {
let unbind
if ('where' in ref) {
unbind = bindCollection(
{
vm,
key,
ops,
collection: ref,
resolve,
reject,
},
options
)
} else {
unbind = bindDocument(
{
vm,
key,
ops,
document: ref,
resolve,
reject,
},
options
)
}
binds.push({vm, key, ref, unbind})
})
}
function unbind(vm, key, reset) {
const i = binds.findIndex(u => u.vm == vm && u.key == key)
if (i > -1) {
binds[i](reset)
binds.splice(i, 1)
}
}
return {
bind,
unbind
}
}
use-rtdb.js
import {rtdbBindAsArray as bindAsArray, rtdbBindAsObject as bindAsObject, walkSet} from '@posva/vuefire-core'
const ops = {
set: (target, key, value) => walkSet(target, key, value),
add: (array, index, data) => array.splice(index, 0, data),
remove: (array, index) => array.splice(index, 1),
}
const binds = []
export function useRTDB() {
function bind(vm, key, source, options) {
return new Promise((resolve, reject) => {
let unbind
if (Array.isArray(vm[key])) {
unbind = bindAsArray(
{
vm,
key,
collection: source,
resolve,
reject,
ops,
},
options
)
} else {
unbind = bindAsObject(
{
vm,
key,
document: source,
resolve,
reject,
ops,
},
options
)
}
binds.push({vm, key, source, unbind})
})
}
function unbind(vm, key, reset) {
const i = binds.findIndex(u => u.vm == vm && u.key == key)
if (i > -1) {
binds[i](reset)
binds.splice(i, 1)
}
}
return {
bind,
unbind
}
}
use-meeting-store.js
import {createStore} from "pinia";
import {useFirebase} from "./use-firebase";
import {useRTDB} from "./use-rtdb";
import {useFirestore} from "./use-firestore";
export const useMeetingStore = createStore({
id: 'meeting',
state: () => ({
id: null,
meeting: null,
participants: []
}),
getters: {
availableSeats(state) {
if (!isNaN(state.meeting?.maxParticipants)) {
const audience = state.participants.filter(p => !p.host)
return state.meeting.maxParticipants - audience.length
}
}
},
actions: {
bind(id) {
this.state.id = id
return Promise.all([
this.bindMeeting(),
this.bindParticipants()
])
},
bindMeeting() {
const {bind} = useFirestore()
const {firebase} = useFirebase()
return bind(
this.state,
'meeting',
firebase
.firestore()
.collection('meetings')
.doc(this.state.id)
)
},
bindParticipants() {
const {bind} = useRTDB()
const {firebase} = useFirebase()
return bind(
this.state,
'participants',
firebase
.database()
.ref(`/participants/`)
.orderByChild('meetingID')
.equalTo(this.state.id)
)
},
createParticipant() {
return new Promise((resolve, reject) => {
const auth = useAuthStore()
const {firebase} = useFirebase()
const database = firebase.database()
const participantRef = database.ref(`/participants/${auth.state.uid}`)
database.ref('.info/connected').on('value', snapshot => {
if (snapshot.val() == false) {
return
}
participantRef
.onDisconnect()
.remove()
.then(() => participantRef.set({
dateCreated: firebase.database.ServerValue.TIMESTAMP,
meetingID: this.state.id
}))
.then(resolve)
.catch(reject)
})
})
}
}
})
Splashscreen.vue
<template>
<div>
<div class="middle">
<h1 class="ui header">
JOIN
</h1>
<div class="ui tiny blue sliding indeterminate progress">
<div class="bar"></div>
</div>
</div>
<div class="bottom">
<p>by</p>
<img src="assets/pitcher-logo-COLOR-TM-300.png" alt="Pitcher AG">
</div>
</div>
</template>
<script>
import {onMounted} from '@vue/composition-api'
import {useAuth} from "../use-auth";
import {useMeetingStore} from "../use-meeting-store";
export default {
name: 'Splashscreen',
props: {
authToken: {
type: String
},
meetingID: {
required: true,
type: String
}
},
setup(props, {root}) {
const {login} = useAuth()
const meeting = useMeetingStore()
onMounted(async () => {
await login(props.authToken)
await meeting.bind(props.meetingID)
if (meeting.availableSeats === 0) {
return root.$router.push({name: 'full'})
}
await meeting.createParticipant()
root.$router.push({name: 'room'})
})
}
}
</script>
<style lang="less" scoped>
.middle {
left: 50%;
position: fixed;
top: 50%;
transform: translate(-50%, -50%);
.ui.header {
font-size: 96px;
}
}
.bottom {
bottom: 35px;
left: 0;
position: fixed;
right: 0;
text-align: center;
img {
max-width: 150px;
}
p {
margin-bottom: .5em;
}
}
</style>
I don't have plans right now to add it but it would make sense for Vue 3. Good news is, as you found out, the core is ready for this change because it binds data to the property of an object. Which means, it could easily work by receiving a Ref and binding to its value property, or to a Reactive object and a provided key property to bind to that property. I think that the next version of vuefire should reunite vuexfire and vuefire together and expose lower level functions that are in core
@torfeld6 are ./use-firebase and ./use-auth also available by any chance?
Thank you for your hard work on this, it looks like terrific progress toward having something working on Vue 3. I'm really keen to avoid going back to a VueFire-less life!
Hi @electric-skeptic,
the project has changed quite a bit since I last posted March 22. I can recommend having a look at the guten-abend project.
The functions I created for the RTDB:
useRef.js
import firebase from 'firebase/app'
import 'firebase/database'
import {computed, reactive, toRefs} from '@vue/composition-api'
export default function useRef() {
const database = firebase.database()
const state = reactive({
loading: true,
exists: false,
val: null,
docs: computed(() => {
if (state.val) {
const res = []
for (const key in state.val) {
if (state.val.hasOwnProperty(key)) {
res.push({
...state.val[key],
key
})
}
}
return res
} else {
return []
}
}),
path: null
})
async function loadByPath(path, filter = {}) {
let ref = database.ref(path)
for (const key in filter) {
ref = ref.orderByChild(key).equalTo(filter[key])
}
const snapshot = await ref.once('value')
state.exists = snapshot.exists()
state.loading = false
state.path = path
state.val = snapshot.val()
return state.exists
}
return {
...toRefs(state),
loadByPath
}
}
useLiveRef.js
import firebase from 'firebase/app'
import 'firebase/database'
import {computed, reactive, toRefs} from '@vue/composition-api'
import Vue from "vue";
const state = reactive({})
export default function useLiveRef(path, filter = {}) {
if (!state[path]) {
const database = firebase.database()
let ref = database.ref(path)
for (const key in filter) {
ref = ref.orderByChild(key).equalTo(filter[key])
}
Vue.set(state, path, {})
ref.on('child_added', data => {
Vue.set(state[path], data.key, data.val())
});
ref.on('child_changed', data => {
Vue.set(state[path], data.key, data.val())
});
ref.on('child_removed', data => {
Vue.delete(state[path], data.key)
});
}
const docs = computed(() => {
if (!state[path]) {
return []
}
return Object.keys(state[path]).map((key) => ({
...state[path][key],
id: key
}))
})
return {
...toRefs(state[path]),
docs
}
}
I prefer guten-abend's syntax. You can use the documents directly in the component or wrap them in another function (i.e. useMeeting or useParticipants).

I went back to just having a simple firebase.js initializing and exporting the components I need. I believe the use-functions are only necessary for view-states. But I'm still trying to figure out how to structure and divide things in Vue 3.

Kind Regards
Joshua
@torfeld6 Thanks so much for sharing all of this - it's definitely an easier starting point than the track I've been wandering down trying to get this working!
Most helpful comment
I don't have plans right now to add it but it would make sense for Vue 3. Good news is, as you found out, the core is ready for this change because it binds data to the property of an object. Which means, it could easily work by receiving a
Refand binding to itsvalueproperty, or to aReactiveobject and a provided key property to bind to that property. I think that the next version of vuefire should reunite vuexfire and vuefire together and expose lower level functions that are in core