How to build and compose reusable components in a re-frame application?

Since I am building multiple (it is five now, I expect more) applications I feel the need to build a library with reusable components. This are my thoughts and problems with the re-frame part.

Say, I have a reusable contacts component, living in the (here simplified) contacts namespace. The contacts.api namespace has functions for managing the contacts data structure (mostly CRUD, like add-contact, remove-contact, contacts, …). Then I have contacts.re-frame which defines subscriptions and events. These mostly just call contacts.api functions with the contacts data structure from the app-db. There is also the contacts.ui-components namespace which contains the react components.

Now the chat application (or component) uses the contacts component. Using the re-frame subscriptions and events is no problem. Using react components also fine. But I strugle how the chat.api namespace should get access to contacts.api. Since chat.api works on its own datastructure which it gets passed from chat.re-frame, it has no access to the contacts datastructure and therefor can not call anything from contacts.api. It seams, the functions from chat.api need to also get the contacts datastructure passed and need to return it besides its own.

In the Java world I tackled such things with dependency injection, I am unsure how this should work in the world of immutability. Am I on the right path? Is there a better way to build this? Thank you for any suggestions or links.

The day8 orgianization has a collection of components they’ve built using Reagent in addition to Re-frame itself. It might be useful to see how they have chosen to build their components for reusability:

re-com has only “simple” reagent components (just the ui). there are no whole reusable modules (with api+events+ui).

I seem to remember reading something like what you are thinking about in the source code for Braid (https://github.com/braidchat/braid). I can’t remember exactly, as it was quite a while ago.

I’ve done some solo work in re-frame, still trying to figure this out myself. I have used Redux pretty extensively in a large team though, with immutable-js and redux-loop.

For cross-db data access in re-frame I tend to just dispatch events from one event handler to the other, passing the required data. If you want to do that without the two having to know about each other, you’d have to pass the data from subscriptions in the component, including possibly passing the event through another handler with its data. The re-frame events are essentially the public API. How you structure your db is an implementation detail.

Another pattern that we used in redux was selector functions. A selector function is expected to be passed the entire redux store. A function called getContactById would look the data up for the caller based on its redux structure. If that structure changes, the selector function could hide that without breaking the caller.

Something like:

import {List} from 'immutable';

function getContacts(store) {
    store.getIn(['some', 'path', 'contacts'], List());
}

function getContactById(store, id) {
    getContacts(store).get(id);
}

Equivalent Clojurescript:

(ns contacts.api)

(defn all [db]
  (get-in db [:some :path :contacts] []))

(defn by-id [db id]
  (get (all db) id))

This was mostly useful because the redux connect(mapStateToProps, mapDispatchToProps)(MyComponent) would pass in the redux store and dispatch function as its arguments. The selector functions would fill the same role as subscriptions in re-frame, grabbing data the component needs without having to know the db structure. You could also use those in unrelated event handlers where you have the entire db passed to you.

@baritonehands Thank you for your info.

Your selector solution requires that the caller has access to the app-db. But I would like the caller (here chat.api to work on it’s local data structure only without access to the app-db. So it can not pass it to the selector. I have already thought of a partial of the selector created by the event-/subscription-handler, where the db is already enclosed. But this works only for read-functions. What if I need to call a modifying function in chat.api which needs to call a modifying function in contacts.api?

I have come up with this “dependency injection+outjection” for now:

  1. The event-handler retrieves both data structures from the app-db.
  2. It puts the contacts data into the chat data
  3. It calls the updating function in chat.api
  4. It gets and removes from the modified resulting chat data the contacts data and puts both back to app-db
(rf/reg-event-db
  :chat/do-it
  (let [contacts (get db :contacts)
        chat (get db :chat)]
        chat (assoc chat :contacts contacts)
        chat (chat/do-it chat)
        contacts (get chat :contacts)
        chat (dissoc chat :contacts)]
     (assoc db :chat chat :contacts contacts)))

Now chat.api/do-it can call contacts.api:

(defn do-it [chat]
  (-> chat
      (assoc :chat-stuff :done)
      (update :contacts contacts/do-with-contacts)))

You can model events and UI using plain reagent too. That’s preferably how you should model your child components, and then wire them together at the app level using re-frame (if that’s what you’re using for state management).

Many libraries in JS land made the same mistake of coupling their functionality to Redux, because it was very popular at the time, but made it difficult to compose these components (especially in applications that didn’t want to use Redux).

Any component built with re-frame can be re-built using reagent and plain functions. Use props to pass in data / elements / event handlers / etc. and then have the component call the event handlers when necessary.

(defn contacts [{:keys [contacts on-add-contact on-remove-contact]}]
  ,,, create the UI and wire up event handlers)

;; Usage

(defn app []
  [contacts {:contacts @(rf/subscribe [:contacts/data])
             :on-add-contact #(dispatch [:contacts/add-contact %])
             :on-remove-contact #(dispatch [:contacts/remove-contact %])])

The key thing is to use components as the primary method of composition, and use re-frame to wire things up at the seams of the application. If you spread your re-frame logic across all your application, it will become less composable, less flexible. You will likely regret it later.

4 Likes

Thank you for this tip.

In your example you deref the data outside the component. Is this somehow better then passing the atom/subscription and derefing inside the component?

1 Like

IMO it’s better to pass in data than a stateful thing like ratom or subscription. There are bugs that can occur when passing in re-frame subscriptions specifically (https://github.com/day8/re-frame/issues/553).

But it also becomes much easier to change where data comes from; maybe you need to transform it a bit in the parent before passing it to the child component. Maybe you want to test the component in isolation, without having to involve a subscription or ratom.

There are some cases where it’s more performant to subscribe in the child component instead of the parent. In that case, you can pass down a function that returns the subscription/ratom/etc. which I think would avoid the bug I linked above.

3 Likes

There is a talk about Modular Users Interfaces with Re-frame from Radford Smith

2 Likes

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.