crisptrutski/matchbox

recommended way to use with re-frame

Closed this issue ยท 11 comments

I'm trying to figure out how to integrate matchbox with re-frame. Do you have any tips or pointers please? I'd be happy to push out an example for documentation.

I'm not convinced this is the best way to do it but here's the key bits of my quick and dirty integration:

(you can see full source here)

(rf/register-handler
  :fb-update
  (fn [db [_ k v]]
    (case k
      :activities (assoc-in db [:planbook :activities] v)
      :lessons    (assoc-in db [:planbook :lessons] v)
      :resources  (assoc-in db [:planbook :resources] v)
      :classes    (assoc db :classes v))))

(rf/register-handler
  :setup-db
  (fn [db _]
    (doall
      (for [{:keys [fb k]} [{:fb fb-activities :k :activities}
                            {:fb fb-classes    :k :classes}
                            {:fb fb-lessons    :k :lessons}
                            {:fb fb-resources  :k :resources}]]
        (m/listen-to fb :value (fn [[_ v]] (rf/dispatch [:fb-update k v])))))
    starting-db))

(rf/register-handler
  :class
  (fn [db [_ command id attribute value]]
    (case command
      :delete (m/dissoc-in! fb-classes [id])
      :update (m/reset-in!  fb-classes [id attribute] value)
      :new    (m/conj!      fb-classes (:class new-default)))
    db))

You obviously lose a lot of re-frame purity and I'm not handling any errors yet!

Great question - it certainly isn't as obvious as using directly with reagent.

I've tried two approaches here, and both worked well enough for the applications, but had their warts.

  1. Use lifecycle hooks to fire handlers to start/stop sync between refs and the db
  2. Cheat and use subscriptions which serve data directly from firebase listener

For (1) this looked similar to @tomisme's example for me, with a few differences:

  1. Used handlers to add and remove listeners dynamically - so that data sync could react to navigation for example.
  2. Rather than using another handler to update the db, I used a non-pure handler and matchobx.atom to directly bind ref listeners to paths in the atom, for memory and performance reasons.

For mutation things were also similar - create domain level events which just call the matchbox mutators directly (and get optimistic updates and rollbacks for free, yay).

The biggest pain point with this approach if I recall was reference counting - had to do some silly book keeping to avoid double listeners being created, but still ensure unnecessary syncs were removed. And for this all to work without gotchas with figwheel, etc.

There's definitely space for matchbox to handle more of this on your behalf, and to do smart things like remove listeners that sync a subset of the data another listener is syncing, but put it back if that listener is removed. PRs welcome ๐Ÿ˜„

Why (2)? Why cheat so outrageously?

The approach I took here was to use matchbox.reagent inside subscriptions to create ratoms. Taken this approach both on smaller projects or on particular sections of larger projects where the data here is fairly ephemeral or curated - the client is syncing a query and not holding all the source data. eg. the top 10 scores for a viral game. Another example is where Firebase is only a side store, say for live chat or something quite disconnected from other domain objects.

The pros:

  1. No setup/teardown handlers and lifecycle hooks
  2. No awkward carving out of app state for one-time or concurrent queries
  3. Just uses mental model of regular Matchbox

The cons:

  1. Not all state in the main atom
  2. Cleaning up listeners behind stale subscriptions is annoying

@Quantisan let me know if anything is unclear, or if your requirements are different. I'm not the hottest of re-frame hotshots, it's actually been a while since I've done any CLJS work, and there could be a better approach.

Cursing myself for losing data to the Slack vortex - actually had a chat with @danielcompton about this exact topic some time ago. Maybe he can recall it better than I do, and purpose even kept the sample code I furnished

Thanks for the feedback, @tomisme @crisptrutski. I've spiked out a chat demo here that seem clean enough. Basically I've taken approach (1) because I'm not too familiar with matchbox yet. However, (2) could be promising as ratom is an implementation detail and it could be anything like a firebase listener or a datascript store. The fact that firebase should work offline too would make it robust.

Regarding my take, there are two places where matchbox is called.

  1. setting up the sync when a user joins a room. https://github.com/Quantisan/re-frame-firebase-example/blob/master/src/cljs/minimal_chat/core.cljs#L74
  2. when user sends a message, I m/conj-to! directly, as you're suggesting. https://github.com/Quantisan/re-frame-firebase-example/blob/master/src/cljs/minimal_chat/core.cljs#L88

I'll need to re-read your responses tomorrow again to make sure that I understand everything correctly.

I have a few immediate questions,

  1. what's the difference between listen-list and listen-to? I thought I would use listen-list for the messages as they're a vector of messages. But I was only getting the first element when I use listen-list. Whereas listen-to works fine.
  2. How come the ordering of the messages that I get back is not preserved? I'm probably missing something here
  3. @crisptrutski you mentioned that you can remove a listener. How would I do that?
  1. listen-list should return a vector, ie. throw away the keys and order according to priority / key. Sounds like you ran into a bug - are you using 0.0.8-SNAPSHOT?
  2. CLJS doesn't provide an unbounded "preserves input order" map, and Firebase represents lists as sorted JS objects, hence adding (1)
  3. You can unsubscribe using the return value from creating the listener or using the ref

Late here and I'm off to sleep, will give feedback on your repo over the weekend - that was a quick spike! ๐Ÿ‘

Oh, a note on (3) is that for the channel and atom based APIs, being able to unsubscribe given the sync target is something I'd be happy to add if there's interest

We're not using Matchbox, but are using a similar (internal) library which presents a Ratom. We make re-frame subscriptions to query the database. We combine the external subscription with app-db if we need to use them together. I'm not 100% convinced this is the best long term solution, but it seems to be working well for now.

Thanks for sharing @danielcompton - out of interest is that internal library also wrapping Firebase? Or is this re-thinkDB?

The internal one is wrapping rethinkDB, but it's the same principle.

Just checking ๐Ÿ˜„

closing this for #47