Fork me on GitHub
#re-frame
<
2022-03-13
>
lassemaatta05:03:51

What's the latest "best practices" regarding subscribing to external data? This question pops up every once in a while (e.g. https://clojurians.slack.com/archives/C073DKH9P/p1625157934260300) and I've yet to find a good satisfying solution.

lassemaatta05:03:30

For context, the typical scenario for me is usually: - We have several API endpoints (`/api/users`, /api/user/{id}/details, /api/hobbies etc.), and we only want to fetch the minimum amount of required data - We have a rich set of subscriptions which enrich, filter, compare and merge data from the endpoints. More importantly, the intermediate results may cause some reg-sub handlers to @subscribe to other subscriptions (e.g. if data from the user details indicates that a user has a hobby we need to subscribe to [::hobby hobby-id], which needs data from the hobby API). - We have views which consist of different components, which subscribe to data and are also shown/hidden depending on subscription data.

lassemaatta05:03:50

One easy (but perhaps not simple) solutions is to do what http://day8.github.io/re-frame/Subscribing-To-External-Data/ suggests: use dispatch within reg-sub-raw, which seems to solve all my problems. But the same document now deems this as "wrong" (without specifying why or what the downsides are). What's the right way?

lassemaatta05:03:48

At this point someone (hi eugene! 🙂) usually suggests either event handlers or interceptors, but I have a hard time figuring out how to implement that correctly. I don't want to start duplicating the logic of the subscription tree, that seems really brittle (view components & subscriptions are constantly added/altered/removed by developers). What I'd really need is a way to ask (most likely within an interceptor) questions such as "does someone require data from an API endpoint and do we have the data for it?". I guess theoretically that might be derived from re-frame.subs/query->reaction, but I assume that brings in a whole new set of problems.

lassemaatta05:03:11

I could move the logic from the subscriptions into event handlers: instead of building a deep tree of subscriptions I could keep them shallow and just calculate everything in the event handlers and store the enriched data in app-db. But then I'd need to add more logic to avoid re-calculating stuff (which is what subscriptions already solve) and make sure the data is fresh (classic caching problem). And it wouldn’t guarantee each subscription got what it needed

p-himik08:03:58

> hi eugene! 🙂 Heheh, hello there. From https://day8.github.io/re-frame/FAQs/LoadOnMount/: > • In re-frame, Components are not causal, they are reactive. > • In re-frame, it is events which are causal (never components). Your current mental picture is that of React - "the component is the boss, it tells everything what to do, it ask for anything that it needs". Re-frame's mental picture is much less focused on the view part. If a component is mounted - it's because some event has changed the right data for it to become mounted. If that component is mounted but lacks some data - it's that event's failure to provide the data (or some other event's - depends on how you structure your app). Just like a parent giving a kid some money and telling them to go buy some bread, events are dictating what views are mounted (indirectly via subscriptions) and they're the ones that must also make sure that the data is there or will be there (maybe directly, maybe indirectly via other events, interceptors, effects).

p-himik08:03:35

I understand how that can be confusing as I occasionally have to deal with that myself. What helps is to restructure at least the relevant parts of your application in such a way so that all things that dictate if a component is mounted are reused. So suppose you now have a function (defn panel-x-visible? [db] ...). That function will be used by a subscription, which will then be used to check whether that panel X should be mounted:

(when @(rf/subscribe [::panel-x-visible?])
  [panel-x])
Panel X needs some data. If the panel always becomes visible thanks to a single event - you can just load the data there, you already have everything you need. But if the panel becomes visible because of arbitrary computations in arbitrary events, you can add a global interceptor that compares (panel-x-visible? old-db) to (panel-x-visible? new-db). If the value of the former is false and the value of the latter is true, then that interceptor would schedule data loading. As a yet another alternative that's suitable for somewhere in between, if there are many events that make the panel X visible but that's done explicitly, via e.g. (assoc db :panel-x-visible? true), then move that assoc into its own function, make it act not on db but on ctx where :db is just one of the keys, make that function also do the data loading part, and then use that function in every event that makes the panel visible..

p-himik08:03:06

Finally, if you deal with such things not once or twice but all the time, then re-frame might either be a wrong thing for you altogether or, likely, just a too low level of a thing. Depending on the complexity of your apps, it might be worth it to build something like om.next on top of re-frame - where a component is not a random function but an entity on its own that specifies what it inputs it needs, what data it will query, what function will be used to render it. Then whatever is managing components will be able to check what data any component needs and retrieve it if needed.

lassemaatta08:03:20

yeah, my difficulty is the "[events are] the ones that must also make sure that the data is there", because the consumers of the data (subscriptions and ultimately the views) have no way to signal what data is actually required. Therefore the event handlers must somehow figure this out independently or, what I typically see, they just eagerly fetch all they can just in case.

lassemaatta08:03:04

(I've browsed through the fulcro docs and like what I see, but at the same time it looks quite intimidating)

lassemaatta08:03:39

if only the re-frame docs permitted me to just use reg-sub-raw, where I could dispatch an event, which would just set a flag in the app-db indicating what's required (and clear it in on-dispose). Nothing fancy, just a way signal that "some subscription(s) need to know the details of users #{1 4 9}". Then I could use interceptors et al to decide what to do (api calls etc) 🙂

p-himik09:03:19

> the consumers of the data (subscriptions and ultimately the views) have no way to signal what data is actually required Right - it's supposed that the events know what is needed. Which is hard to achieve in a complex app with lots of nested components... > I've browsed through the fulcro docs and like what I see, but at the same time it looks quite intimidating Same, with one extra concern - there's still a lot of churn going on. It all feels like an R&D project. > if only the re-frame docs permitted me Those are just docs, not cops. :D

lispers-anonymous14:03:20

Every so often at my job I find a bug that was caused by a developer struggling with this question (maybe not consciously, but it always boils down to this problem). They have a component and it needs some data from an API, or it needs to put some data in app-db, whatever. The component might be used in two spots, so they decide to dispatch in the body of a component. It is usually in a reagent with-let block, so it only runs when the component mounts, but sometimes it's in the actual render function. It might work okay when it's first developed, but something else in the component tree changes and the bug surfaces where reacts gets caught in an infinite rendering loop. There is a tradeoff made when you decide that subscriptions and components can kick off side effects like fetching external data: you are probably handing over control of that data's lifecycle to react. Now something higher up the component tree can change and cause your component to render more often than expected, or even dismount and remount and cause carefully crafted form-3 components and reg-sub-raw subscriptions to be re-created and unnecessarily re-fetch that data. It is possible to pull this off, developers using vanilla react do this a lot, but it is hard to reason about when side effects happen, and a lot easier for them to get out of control.

👍 1
lispers-anonymous14:03:57

All that said, the ultimate question of "I have a component that requires some data, how do declare that it needs that data" is still hard for me to answer. Right now I just have to understand where the component is used, and make sure that whatever user driven event causes that component to be displayed also fetches the data (usually navigation, so the router kicks of the request). It can be error prone but once everything is in place it usually protects us out of control rendering or hitting our expensive APIs more often than necessary.

lassemaatta15:03:05

I certainly agree. I think that with simple apps this problem doesn't necessarily surface as much. Your #/user/<id> view probably needs data from your /api/v1/user/<id> endpoint so initiating the fetch in the appropriate route event handler is straightforward. But when your APIs and views don't map 1:1 so cleanly things get complex. A very common bug: navigate to a view and refresh the browser -> some of the data disappears, because a poor developer (sometimes myself) didn't notice that the view only worked by chance because some of the data was fetched earlier for another view. The way I see it is that re-frame offers us a) a bunch of really nice mechanisms and tools (events, dispatching, subscriptions..), b) a general philosophy on how a re-frame app should work and c) concrete guidance on how to actually use the provided tools and what (not) to do. A majority of the guidance makes a lot of sense to me (try to avoid side-effects, don't build generic "path" subscriptions etc.), some of the guidance goes into the "umm.. ok, sure, I guess that's a good idea" bucket (e.g. how to name events) and some guidance I haven't yet really grasped (this issue). And of course over the years I've seen most of these rules broken in a variety of apps. Sometimes accidentally, sometimes intentionally. Sometimes causing issues and sometimes not.

oliy11:03:10

have been using the reg-sub-raw + dispatch + cleanup for years, never had any problems with it. benefits: • self contained (encapsulated) and therefore... • reusable in other places without them having to know how to dispatch an event to be able to use the sub • also reduces code entanglement - if you have to dispatch an event to use the sub, the sub and the event are co-dependent and you can't use one without the other • lazy - only fetches data when you know something is asking for it, rather than eagerly pre-fetching something which may never be required which costs memory and network • avoids the issue you described where you are using a sub which only works because some other page load happens to have fetched the data, but breaks when you refresh • allows my SPA routing to do one job of deciding what view to display based on the url, rather than having an extra job of firing re-frame events, and having to work out which re-frame event goes with which url/view there is a sort of hybrid which is adding a dispatch on your view's component-will-mount lifecycle to fire your 'init' event, which can fetch any data you know is needed. fixes the last point i made above, but the other ones are still relevant