Skip to content

Instantly share code, notes, and snippets.

@tylerperkins
Last active February 10, 2018 20:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tylerperkins/d6c7cfa11e251ba3c62a9f91665c67db to your computer and use it in GitHub Desktop.
Save tylerperkins/d6c7cfa11e251ba3c62a9f91665c67db to your computer and use it in GitHub Desktop.

Using owlet.firebase

Namespace owlet.firebase serves to integrate a Firebase database into our re-frame web application. We need to talk to Firebase without dropping into evil JS interop — so you shouldn't use .set! But more generally, we must preserve the integrity of our application's re-frame data flow.

re-frame data flow

In the description below, I've tried to be entirely consistent in my use of names; i.e. the scope of any name is this entire document. So when you see your-value or the-db-ref, you can count on those names indicating the same entities when they appear later in the document. Also, "ref" or "reference" refers to an instance of firebase.database.Reference, not a Clojure ref.

Saving a data value to the Firebase database

  1. Your data value must satisfy the predicate (owlet.firebase/legal-db-value? your-value). Basically, this just means your value must be convertible to JSON. So, nil, booleans, numbers, strings, keywords, and symbols are all OK. Also, non-empty maps and finite non-empty sequential things containing legal values are themselves legal values. So vectors, lists, sets, and maps are all OK, but maps may only have keys that are strings, keywords, or symbols.

    • N.B.: The value nil (or any empty collection) is never actually stored. Instead, "saving" nil just results in deleting the value at the given Firebase ref, if any. That's just how Firebase works, and this is the canonical way to remove a node from the database. Also note that every collection is really saved like JSON, so if you save a set, for example, and then later you retrieve it (with, say, on-change), you will just get a vector of its values back, not a set.
  2. Send your data value to the desired Firebase database reference. In the following, let's assume we've defined the-db-ref by calling function owlet.firebase/path-str->db-ref with the desired path string as argument.

    Now, talking to the database is an effect, so the handler invoking it should be registered with reg-effects-fx and should return a map like the following:

    • If your-value should go into the list at the-db-ref:

      {:firebase-reset-into-ref
       [the-db-ref your-value :your-event-id args ...]}
      
    • If your-value should replace the node (if any) at the-db-ref:

      {:firebase-reset-ref
       [the-db-ref your-value :your-event-id args ...]}
      

    Here, the :your-event-id args ... elements are optional. You may not need any args, say if you only want to know whether the save was successful. Or you might not even provide an event id keyword if you don't care. But if you do provide an event id, then when the attempt to save your data to the Firebase database has finished, a save-finished re-frame event is dispatched:

    • If the save was successful: [:your-event-id {:ok-value nil} args ...]

    • If the save was unsuccessful: [:your-event-id {:error-reason r} args ...], where r is an instance of JS class firebase.FirebaseError.

  3. Handle your save-finished event, [:your-event-id ...]. Typically, you'll want to save your value in app-db as well, so your GUI can display it to the user via a subscription. But this should only be done when we're sure we've succeeded with the save to Firebase. Just define a :your-event-id handler like this:

    (rf/reg-event-db
      :your-event-id
      (fn [db [_ {err :error-reason} your-value]]
        (when-not err
          (assoc-in db [:path :to :data] your-value))))
    

    The thing to notice here is that your-value is being sent to your handler when the save completes. But how does that happen? Well, we have to provide it when we issued the effect :firebase-reset-ref or :firebase-reset-into-ref:

    {:firebase-reset-into-ref
     [the-db-ref your-value :your-event-id your-value]}
    ;                       ^^^^^^^^^^^^^^ ^^^^^^^^^^
    

    We've simply provided your-value as the arg to be sent to the :your-event-id handler. We could just as well have sent more info about the data, like the particular key under which the value should be stored in app-db.

    Notice also that the second element in the event vector (after the _ id) is the result map. It's either {:ok-value nil} or {:error-reason r}. Above, we just checked for the existence of key :error-reason.

Number 3, above, is the most common way to keep app-db in sync, but another way might be preferred if other code can touch the-db-ref. This could happen if another client modifies your value on Firebase. In this case, just employ owlet.firebase/on-change to register a handler you write to respond to any change in the value at the-db-ref.

For example

You can see an example of all this in action starting at handler :update-user-background! in owlet.events.app. It is dispatched when the user has succeeded in saving a background image to Firebase Storage. It issues the :firebase-reset-into-ref effect, passing not just new-image-info as the new data to be stored in app-db, but also the existing value, old-image-info, in case the save to the Firebase database fails. Both cases are found in the indicated handler, :user-background-saved, including the removal of the old image file from Firebase Storage if all goes well!

The design of owlet.firebase helps keep our code simple and ClojureScript-clean. It hides the complexity of JavaScript's call-backs and promises for communicating with Firebase. I hope it helps us to fully embrace the benefits of Derived Values, Flowing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment