Skip to content

Instantly share code, notes, and snippets.

@mike-thompson-day8
Last active May 14, 2020 08:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mike-thompson-day8/37e85576a3b6441ed7e8be7c5457cac1 to your computer and use it in GitHub Desktop.
Save mike-thompson-day8/37e85576a3b6441ed7e8be7c5457cac1 to your computer and use it in GitHub Desktop.

this is an early draft. Better to now look at: https://gist.github.com/mike-thompson-day8/dad5b66c8cd74082326dad6ce331128d

re-frame components

There are big, composite components and there are small, simple ones.

Reagent components are simple components - often just called widgets. They visually represent a simple value like an integer or a string, or a selection. A library like re-com provides many Reagent components including dropdowns, and Text Input fields and radio buttons.

re-frame components are larger, composite components. They tend to visually represent an entity (a more complicated thing) rather than a single, simple value, and they typically present to the user as a "widget-complex" (many widgets) with a cohesive purpose. For example, a pivot table would be a larger component. It might supports the drag and drop of fields to "shelves" which configures a table showing data rollups, and totals. And it might include widgets which allow for levels to be expanded and collapsed, and perhaps "reset" and "copy to clipboard" buttons (which might need to be greyed out or not subject to the current configuration of the pivot).

Responsibilities

Irrespective of being big or small, components have two responsibilities:

  • to render a representation of the thing they are modelling. So, first, the user needs to see the current value and, optionally, there may be some "affordances" also rendered, making it clear to the user, how they could manipulate the value.
  • to accept and interpret user actions, like clicks, typing, and dragging, performed on that visual representation and, in response, to either change the widget's appearance and/or communicate to the surrounding app how the user wants to change the value.

Needs

To perform its function, a component (big or small) needs to:

  • obtain the current value and be informed about any updates to that value over time. With larger components, the sub-components need to source sub-parts of the overall entity value being shown/edited.
  • communicate user-initiated changes to the value. When the user interacts with a component, they are trying to change a value, which must be communicated to the surrounding app. With a larger, composite component, the user will interact in many ways, making many kinds of adjustments to different parts of the entity.

How - Reagent Components?

With Reagent components, like re-com widgets:

  • they obtain the current value as args/props. The parent Reagent View will source this data somehow, and then supply it. The child Reagent component will be oblivious to how the data was obtained.
  • they communicate changes in value by invoking "callback functions" supplied as args/props. Again, the Reagent component is oblivious to its surrounding context, and what actions the callback might take. We, as programmers, might know that the callback causes a re-frame event to be dispatched , but the Reagent component knows nothing of re-frame, dispatch or app-db.

Reagent components might not know about re-frame but, unsurprisingly, re-frame components do.

How - re-frame Components?

With re-frame components:

  • sub-widgets obtain values via a subscribe (and not from props, like Reagent components)
  • signal via dispatch (and not by invoking a callback, like Reagent components)

If there are many instances of a re-frame component, how do they subscribe to "their" specific value? One instance of the component might represent entity A and needs to subscribe to data for that entity, and another might represent entity B. How should this happen?

Answer:

  • re-frame components need to know the identity of the entity to which they should subscribe.
  • and it should then supply that identity when it uses subscribe
  • And then, the subscription handler will need to use this identity to locate the entity

Identity

An identity is something that can be used to differentiate one entity from another within app-db. Typically, an identity is a sub-path within app-db. An identity is always a piece of data.

At a minimum, that might be a key in some map (within app-db), like "1278" or :outside. Or it could be the integer index into some vector (again, within app-db). Or it could be the fully qualified, multistep path from the root ofapp-db right down to some leaf element, like [:active "customers" 187] . Or anything in between.

In theory, an identity can be anything that can be mapped to data in app-db, perhaps even in some multistep process. It is just that, practically speaking, a sub-path tends to be the most natural kind of identity.

Supplying Identity

So, when we create a re-frame component, we supply it with the identity of an entity.

An identity is just data and we can supply it as an arg to the component - and for discussion purposes, let's call that arg id.

Any call to subscribe within the component will provide that identity, perhaps like this (subscribe [:customers id :name]). Notice the use of the id.

And, also, any dispatch made within the re-frame component will also supply id. (dispatch [:doctor id :validate-parking true]). Again, notice the use of id in the event.

The query handlers for the subscription, and the event handlers for dispatch, can be written in terms of that identity provided, allowing them locate the right data in app-db.

In this way, generalized re-frame components can be created.

So, all good? Are we done? Not quite yet.

Sufficiently Complex Components

For any sufficiently complex component, passing id around can be a drag. In a bad case, we might end up with the old "prop drilling" problem in which id is passed deeply into nested layers of a complicated set of sub components.

In such cases, we could use BranchScope. <--- new re-frame feature

That would allow us to place id into the "environment" of the entire branch representing the re-frame component.

This process can nest. High-level identities can be combined with next-level sub-identities.

@p-himik
Copy link

p-himik commented May 8, 2020

Two issues that I see:

  1. Mostly a nitpick. The "to accept and interpret user actions" item in the "Responsibilities" section conflates user actions and responding to data changes. A user action means a call to dispatch from the POV of re-frame. But dispatch doesn't mean it was a user action - it can come as a result of e.g. a message over a WebSocket connection. Also, dispatch doesn't mean data changes. In other words, there are user actions that don't change any data, and there are data changes that aren't a result of any user action.
  2. This one is important. The "obtain the current value" item in the "Needs" section says that if a component has sub-component, that the sub-data is within the data. In general, this is not the case. Particularly so when you normalize the data. The sub-identity can sometimes be derived from the identity (and maybe from the data that it points to), but not always. In the end, it can result in passing multiple ids of different kinds into the same component, or maybe even id with a derive-sub-id function.

Regarding the second point. It's a fuzzy line between "a complex component with many parameters" and "completely different components". I must admit, I don't know exactly where that line is, or even how to start attempting to formalize it. Ideally, such a line should not be defined by re-frame or any other library/framework - it should be a job for the user to decide where they stand (unless there's some perfect choice). I really hope that it's possible to achieve such flexibility, but at the same time I'm pretty pessimistic. Likely, some sacrifices will have to be made.

@mike-thompson-day8
Copy link
Author

mike-thompson-day8 commented May 9, 2020

@p-himik
Many thanks for the feedback. It is very useful to get other eyes on these things.

Notes:

  • on point #1, you are right but, as you say, it is a nit pick, and I won't clarify further in the document
  • on point #2, I will add a new section to the document to discuss that.
  • regarding your comments at the bottom, I agree that these matters are outside of re-frame and more about the design and use of the app's "data model" in app-db.

Result: I added this new section ...

Many Identities

A larger, more complex component can often need more than just a single identity.

For example, a component might need the identity for a list of "things", which act as alternative values a user could choose (think dropdown), and also need to know the identity of the current choice held elsewhere within app-db. This component needs two identities.

In other cases, a sub-idnetity (for a sub-component) can sometimes be derived from the primary identity. Other times, the primary entity contains within it the identity of the other entities.

Ultimately, we leave re-frame and start discussing the pros and cons of the "data model" you have created in app-db.

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