Suggest an edit

Recipes

Syncing to Firebase

The example below shows a recipe for keeping Homebase React in sync with Firebase. client.addTransactListener(callback) lets you listen to every local transaction and send those updates to Firebase. We also need a way to sync Firebase with Homebase React. In this example we create a namespace on Firebase for each user based on their firebase uid and listen to all changes in that namespace. client. transactSilently(tx) allows us save changes received from Firebase without triggering our transactListener function and sending those changes back to Firebase endlessly.

1import { HomebaseProvider, useClient, useEntity, useTransact } from 'homebase-react';
2import firebase from 'firebase/app';
3import debounce from 'lodash/debounce';
4import React from 'react';
5
6const SyncToFirebase = () => {
7    const [client] = useClient()
8    const [currentUser] = useEntity({ identity: 'currentUser' })
9    const userId = currentUser.get('uid')
10    const transactListener = React.useCallback(
11      (changedDatoms) => {
12        const cardinalityManyAttrs = new Set([]) // E.g. ':project/todos' or ':user/friends'
13        const localOnlyAttrs = new Set([]) // E.g. ':current-user/uid' these are attributes you don't 
14        // want to save to Firebase, but also don't want to have to call `client.transactSilently()` everytime you change them.
15        
16        // Find the datoms that were changed more than once
17        const numDatomChanges = changedDatoms.reduce(
18          (acc, [id, attr]) => ({ ...acc, [id + attr]: (acc[id + attr] || 0) + 1 }),
19          {},
20        )
21        // Only send one change to firebase per datom
22        const datomsForFirebase = changedDatoms.filter(
23          // eslint-disable-next-line no-unused-vars
24          ([id, attr, _, __, isAdded]) => !(!isAdded && numDatomChanges[id + attr] > 1),
25        )
26        datomsForFirebase.forEach(([id, attr, v, tx, isAdded]) => {
27          if (!localOnlyAttrs.has(attr)) {
28            const ref = firebase.database().ref(
29              // This example uses firebase realtime database with the following rules.
30              // {
31              //   "rules": {
32              //     "users": {
33              //       "$uid": {
34              //         ".read": "$uid === auth.uid",
35              //         ".write": "$uid === auth.uid"
36              //       }
37              //     }
38              //   }
39              // }
40              // Every user has a unique namespace with full read/write permission.
41              // For single page apps like this we can write the raw datoms to this namespace.
42              // Here we are generating a unique key for every datom.
43              `users/${userId}/entities/${id}|${attr.replace('/', '|')}|${
44                // add the value to the key of cardinality many datoms since they are only unique when their value is included
45                cardinalityManyAttrs.has(attr) ? v : ''
46              }`,
47            )
48            // eslint-disable-next-line no-unused-expressions
49            isAdded ? ref.set([id, attr, v, tx, isAdded]) : ref.remove()
50          }
51        })
52      },
53      [userId],
54    )
55    
56    React.useEffect(() => {
57      const softTransact = (tx) => {
58        try{
59          client.transactSilently(tx)
60        } catch (er) {
61          tx.forEach((txPart) => {
62            try {
63              client.transactSilent([txPart])
64            } catch (err) {
65              // eslint-disable-next-line no-console
66              console.warn(err, txPart)
67            }
68          })
69        }
70      }
71      // Homebase -> Firebase
72      client.addTransactListener(transactListener)
73      // Firebase -> Homebase
74      const ref = firebase.database().ref(`users/${userId}/entities`)
75      let txQueue = []
76      const debouncedTransactQueue = debounce(() => {
77        softTransact(txQueue)
78        txQueue = []
79      }, 300)
80      const onAdd = (ds) => {
81        txQueue.push(['add', ...ds.val()])
82        debouncedTransactQueue()
83      }
84      const onRetract = (ds) => {
85        txQueue.push(['retract', ...ds.val()])
86        debouncedTransactQueue()
87      }
88      ref.on('child_added', onAdd)
89      ref.on('child_removed', onRetract)
90      ref.on('child_changed', onAdd)
91      return () => {
92        client.removeTransactListener()
93        ref.off('child_added', onAdd)
94        ref.off('child_removed', onRetract)
95        ref.off('child_changed', onAdd)
96      }
97    }, [userId, client, transactListener])
98    return null
99}
100
101export default SyncToFirebase;