Fork me on GitHub
#re-frame
<
2021-09-09
>
tugh12:09:35

I would like to let you know about https://puppetapi.com/ since it's UI is written in cljs using re-frame. PuppetAPI lets you mock your API calls and you can easily build&share mock endpoints with the ability to view real-time request logs. https://github.com/burkaydurdu/mock-ui

👍 4
2
Elliot Stern18:09:13

Are there any best practices in reframe around creating re-useable components? Particularly, around avoiding collisions in the app-db.

p-himik18:09:15

The most flexible practice is to write all truly reusable components using Reagent. Just like re-com does.

emccue19:09:32

I have one, but it doesn't work well/at all with subscriptions

emccue19:09:49

and I don't think its ready for prime time

emccue19:09:03

but essentially if you can wrap all events coming out of your component into a global/higher level scope and also manage massaging an update loop for the component within that you can do it

p-himik19:09:49

I believe this approach is also described in that issue.

emccue19:09:07

> Yes, I been contemplating this move from a framework to library for a while. It is coming. > It is coming. > Dec 9, 2015

p-himik19:09:04

:D Yeah. But to be fair, this particular problem exists in other UI frameworks. And the main issue here is that fixing it in any easy way also removes some of the features of re-frame. So, alas, the way to fix it for everybody without making re-frame worse in important aspects is far from being easy.

emccue19:09:37

(defn generate-local-component-event-handler
  [handlers]
  (fn handle-event
    [{:keys [shared-state
             component-db
             wrap-event
             event]}]
    (let [[event-type & _] event
          handler          (get @handlers event-type)]
      (if-not handler
        {:component-db component-db
         :component-fx (console-fx/error (str "Can't find handler for " event-type))
         :shared-state {}}
        (let [result (handler {:shared-state shared-state
                               :component-db component-db
                               :wrap-event   wrap-event}
                              event)]
          (-> result
              (update :component-fx #(if (nil? %) fx/none %))
              (update :component-db #(if (nil? %) component-db %))
              (update :shared-state #(if (nil? %) {} %))))))))

emccue19:09:55

some pretty non-specific code that should give you an idea of the approach we are going for

emccue19:09:27

(let [{:keys [component-db component-fx shared-state]}
          (verification-flow-events/handle-event
           {:shared-state db
            :component-db (get-in db [::model/page-state :verification-flow])
            :wrap-event   verification-flow-event
            :event        event})]
      {:db (-> db
               (merge shared-state)
               (assoc-in [::model/page-state :verification-flow] component-db))
       :fx component-fx})

emccue19:09:43

theres a good deal more glue, but basic idea is take in

shared-state: so you can read/write to global stuff
component-db: the state for this component stored somewhere else
wrap-event: A function from "component event" -> "global event"
event: The "component event" to handle
And return
component-db: the state of this component after the event
component-fx: The fx the component wants to perform. Any callbacks should be wrapped
shared-state: The shared state updates the component wants to make

emccue19:09:11

the return values all line up vertically but the input ones don't - thats the worst part of the pattern IMO

emccue19:09:45

but also it doesn't support or account for subscriptions in any way

emccue19:09:29

@U06CM8C3V about to watch your talk, but curious if you have any initial thoughts

manutter5119:09:53

It’s definitely an interesting approach. I might be too deeply embedded in my own approach to evaluate it fairly tho.

lispers-anonymous02:09:22

Every approach I have seen involves passing at least one value to all the smaller components that compose together in the reusable component being dealt with. (maybe those are called molecules or organisms from @U06CM8C3V’s great conference talk that was just posted in the channel) Whether that piece of state is a ratom, a single identifier that is used as part of a path into app-db, or the entire base-path into app-db, something needs to be passed around. I've found passing a base path to be the easiest thing to use. Provide the base path to each component, then pass it through to events and subscriptions. It's easy enough to write helpers for handling the base-path when registering subs and events. It's also not too hard to refactor existing components to use a base path when you find that you need to display two instances of it on the same page without breaking the structure of app-db (existing usages provide the existing base path for the component as an argument, and no one else who relies on that app-db state breaks). I've tried providing these arguments like a base-path to sub-components via react context, but I find it to be way more trouble than it's worth. React context ends up using plain #js objects instead of clojure data. So there is a lot of conversion back and forth. Then everyone needs a context consumer. No thanks. Maybe some macro magic could save me there but it doesn't seem worth it.

lispers-anonymous02:09:06

I have been thinking about these issues a lot. It's been a really difficult problem to solve at my job where we have a HUGE frontend written with re-frame. In the early days we did not have much discipline around how we wrote components and managed app-db state. There are components where we reach into other parts of app-db that are seemingly unrelated. Many different strategies for making them re-usable (we have a table that uses ratoms, it's own events/subs, a passed in identity, data fetched from an API, and passed in event/sub vectors and data). Now we have a big mess that is going to take a long time to clean up. The biggest thing we could have done from the beginning was give ourselves some standards to follow. Just shooting off the top of my head 1. A complex re-usable component can only use values provided in arguments, and values it creates and stores in app-db, managed through a base-path provided at the compoent's entry point. The exception would be that it can read from some pre-defined global store in app-db (a place where the currently logged in user lives, or release toggles for example). Shared subscription namespaces would provide a public interface to that global data. 2. Smaller re-usable components should just operate on simple data, functions provided to them, and ratoms they manage. For example, a button or a text field. But be careful with passing anonymous functions from components that get re-rendered a lot (https://day8.github.io/re-frame/on-stable-dom-handlers/#but-wait-functions) 3. Most components should be written as re-usable components, even if they are only used on one page/route right now. Expect that to change. 4. Components should clean up their data when unmounted, super easy when everything is in a base-path. 5. Global data used by many components should live in it's own space in app-db, maybe under a key called :entities or :data . It should be populated when the application starts, and when certain "global" events take place. 6. Pages/routes should be responsible for fetching and managing their own data, and can't reach into other routes' app-db space or events/subs (we have a special linter that helps us enforce this by inspecting namespace requires). 7. Pages/routes should provide their data via arguments to re-usable components. Those components might pass that data around as arguments or store it in their own part of app-db via a base-path. 8. Try to converge on one pattern for how events and subscriptions are passed to reusable components. Ideally something like a partial event vector. Even better if you standardize around events/subs a fixed number of arguments, like 2. The first is some pre-configured map argument, the second another map conj'd into the event vector. Whatever it is, try to setup guidelines and stick to them where possible. Call it out in docstrings when the guidelines don't work. Something like this might be seen a lot.

(defn my-component [base-path {:events/keys [on-change]}] ...
;; s.t. on-change is an event vector [:handle-on-change {:pre-configured :args}]
  [:button {:on-click #(rf/dispatch (conj on-change {:val %}))}])
9. Write good documentation for what each re-usable component expects. What signature the events and subscriptions should have. What other data it expects should be shaped like. Etc. 10. Use something like devcards, nubank workspaces, storybook js to test your components in isolation. It should be possible to fully setup a component with mock data in a dev card, and two should be able to operate independently on the same page without stomping on each other's state. This is the thing I really wish we had done from the beginning. Most other things would follow. 11. Kind of unrelated, but any components pulled in from NPM needs to be wrapped and some kind of adapter namespace. JS developers seem to break their APIs all the time and it's a pain to upgrade if usage is scattered around your application. Isolate them and treat them as implementation details of your own component wrapping them.

p-himik07:09:35

> managed through a base-path provided at the compoent's entry point Some components genuinely need to access more than one place. Sometimes it can be modeled via a single path and a bunch of data arguments, but it doesn't always make sense. > Components should clean up their data when unmounted, super easy when everything is in a base-path. It doesn't really follow the re-frame model, assuming by a component you mean a view. Views don't own the data that you pass them but something that uses those views might. > Pages/routes should be responsible for fetching and managing their own data Depends on the app. I have one that has pages that have common data that's not used by any other page. In any case, I'd urge anyone interested to not continue this thread and instead comment in issue https://github.com/day8/re-frame/issues/137 if that information hasn't been presented yet. Otherwise, it will get lost - would be a shame for that to happen to some new idea.

lispers-anonymous12:09:54

You make good points, I'll consider commenting on 137 sometime (this info is also in my notes). Right now I'm still going through a process of refining all of these thoughts for my job. I just thought they might be helpful here. Just to respond > Some components genuinely need to access more than one place. Sometimes it can be modeled via a single path and a bunch of data arguments, but it doesn't always make sense. This is true. In that case I'd say pass more argument to the component if it needs to be re-used. If that's not happening, the component isn't very re-usable and can probably only exist in a certain context. That's okay, but I'd probably explicitly note that in a docstring, i.e. this component only works in route foo because of these requirements. > It doesn't really follow the re-frame model, assuming by a component you mean a view. > Views don't own the data that you pass them but something that uses those views might. This I agree with to some extent. If a component is unmounted any data that it has created and is managing I still think should be cleaned up. I say this because we have a had a lot of bugs that are traced down to things not being cleaned up and stale state lingering. The next time a user navigates back to the same page (probably representing a different entity) and the stale state is there when it should not be. If it's not something the component manages, then it doesn't clean it up, something else might or it's global data. We've also handled that by having a component clean up it's app-db data when it's first mounted during some kind of initialize event. > Depends on the app. I have one that has pages that have common data that's not used by any other page. This I think would be managed as some kind of global data in the UI, explicitly called out by being managed separately from the pages themselves. Maybe the router will handle ensuring that data is fetched when navigating to one of those pages. That's still something we're trying to figure out how to do in a manageable way.

p-himik12:09:27

> If a component is unmounted any data that it has created and is managing I still think should be cleaned up But not by the view. Unmounting by itself doesn't mean that the data is not needed anymore. Consider e.g. hiding some large panel via unmounting, just because it's more performant to remove a chunk of DOM than to render it only to hide it. When a view is genuinely no longer needed, it will be communicated via an event. That very same event should do the cleanup. The path for views in your model is provided externally, the decision to show a particular view also comes externally (relative to the view itself) - so the decision to remove a view's data should also be external. A view is but a dumb representation of state and a conveyor of user intent, nothing more - at least, in re-frame's worldview.

lispers-anonymous13:09:03

> When a view is genuinely no longer needed, it will be communicated via an event. That very same event should do the cleanup. This is where we have really struggled. Identifying those events, and then having to know what data to clean up, typically all the data under one path into the db is sufficient, but not always. When adding a new components to a large page, now we have to remember to make sure that it's wired into some cleanup logic that is way up the component hierarchy. Conversely, similar things happen with data requirements that are controlled by some event happening in a place that is "far" away. We've seen a lot of production bugs from that.

p-himik13:09:31

But why does the reverse work - when an event if fired that's supposed to make some view visible, how do you know that you also need to load the right data? How is this situation different from an event that needs to hide a view and cleanup the data?

lispers-anonymous13:09:48

I almost typed in that last response that this is kind of the inverse of loading data requirements lol. We just have to know and then make sure something is getting that data first. And sometimes we forget to do that. These are like implicit inputs to a component. When there are a lot of implicit inputs it becomes difficult to juggle them.

p-himik13:09:05

Right. It reminds me of having to use malloc and making sure that there's a corresponding free call somewhere. Wonder is there can be something like smart pointers here...

🎯 2
p-himik13:09:23

I mean, without the downside of using :component-will-unmount.

lispers-anonymous13:09:36

If there is I haven't found it yet. We made a decision a while back that before we try to solve that, we're going to try to clean up a lot of these implicit data requirements first. A lot of the ones we have are unnecessary, and it will get us pretty far to make more components explicitly declare all the state they need as arguments. Once that is done there will be less instances of this exact problem (but it will still exist). Maybe we will have some way to declarative say what app-db data a component depends on, but we'll still have to figure out how to manage it in a consistent way.

👍 2
manutter5119:09:05

@elliot.stern You might be interested in the talk I gave at the 2018 Clojure conj. https://www.youtube.com/watch?v=JCY_cHzklRs

👏 2
manutter5119:09:31

It’s not without its warts, but we’ve been using it in production with a fair degree of success for the past few years. I’d do things a bit differently now if I had to start over, e.g. it was a mistake to bundle field labels in with the input fields they describe, I’d keep them as separate components if I had it to do over. Also I’m less fond now of the idea of making components out of things that don’t really have any state to manage--just use plain old DOM elements and CSS for simpler stuff.

👍 2
emak21:09:42

Today I got interested about state machines in UI. I can think of too many badly designed components I implemented mixing local state and re-frame app-state that became a hell to debug and maintain over time. Using state machines persited in app-db looks like a promising way to clean up some ugly logic that pollutes these views. https://cognitect.com/blog/2017/8/14/restate-your-ui-creating-a-user-interface-with-re-frame-and-state-machines Are UI state machines common in your codebase?

p-himik22:09:21

Alas, not that common - I definitely should use them more often. But perhaps you'll find these links useful: • https://github.com/jiangts/re-statehttps://github.com/MaximGB/re-statehttps://lucywang000.github.io/clj-statecharts/docs/integration/re-frame/

👍 2
isak22:09:42

@UC0JV84JF in your app, is doing less mixing of local and app-state not an option? Because that would be my instinct from that problem statement - not state machines. I haven't seen any need for state machines personally. Not obvious 1) if/when the benefits outweigh the costs, or 2) when they should be used, and when they should be avoided, etc.

👍 2
javi07:09:41

i have used xstate for a while… currently in my project i am exploring using statecharts in the high level modeling of multi-device experiences…