Fork me on GitHub
#re-frame
<
2016-04-03
>
lwhorton15:04:46

I was wondering if someone could point me to some thoughts on structuring a large re-frame app for success/scalability? Right now as I experiment with an app I have little warning lights flashing in my head for these pieces: 1) how to structure / where to put a particular component’s state? i really like looking at the app-db as a real database with relatively flat, relational data. it makes the data more malleable and less coupled to the app’s component structure. however, each component’s state has to go somewhere - where do forms that are not yet submitted put their input state, for example? consider a db with a list of users {:users [] } entry. if a form utilizes these users to generate a list of checkboxes, where do we store “this user is checked” when a checkbox is ticked? we probably shouldn’t store it right with the user’s entry… because that data has nothing to do with the form component. 2) what’s the best way to update the db at multiple keys/depths inside a handler? this might just be an artifact of clojure… but I find it really inconvenient / verbose to have to update more than 1 key at a time inside the handler function… it turns into a mess of update-in [some path] (blend (the new data) (the old data)) 3) how do we manage a plethora of events and handlers? already i’m running into verbose events with input-changed vs a-different-input-changed vs another-module-input-changed

cky15:04:44

2. You can chain multiple update-in calls with the -> threading macro, which I find helps me a lot. Not sure if it completely solves your use case, but.

nidu15:04:09

@lwhorton: you can use namespaced handlers like :module/action. Do you create re-frame handler for each input change?

cky15:04:44

1. It's okay to have a separate ui-state section of your app-db (doesn't actually have to be structured like that, of course, just however it makes sense for what you're trying to do). Remember it's up to the subscription functions to weave the data together for views; don't mangle your model for the sake of the view!

cky15:04:15

@nidu: Nice, that works great!

lwhorton15:04:43

@nidu that’s what I was going to do, but I was probably going to formalize it somehow so that /app/cljs/some_module/ { dispatch [:some-event] } would automatically be mapped to [:some-module/some-event]

lwhorton15:04:01

less boilerplate the better imo.

lwhorton16:04:08

@cky I figured as much, I’m also chaining update-in via ->, it just feels really cumbersome. perhaps something like spectre will help out here, though I haven’t played with it very much. I was considering having app-db {:components {:some-component-uuid}} be a special place in my db for ‘local’ state, so it’s good to know we’re on the same track.

nidu16:04:33

I've also seen here recently approach to use ::event which expands into :module/event.

lwhorton16:04:08

@nidu should I not have a handler for each temporary state like input boxes? its very transient state, and in other apps I generally only update the app-state when someone is “done” typing, but I figured this time around I would just trust the system and do absolutely everything through the global app atom.

nidu16:04:02

@lwhorton: As for me form usually represent some object state, e.g. login form represents {:user "" :password ""}. So if i'd like to keep state for this form, i'd have handler like :login-form/update which takes this entire object. In input on-change handlers thus i'd have smth like (rf/dispatch [:login-form/update (assoc data :user %)])

lwhorton16:04:38

^ that’s neat… but I have no idea how macros work yet (which I assume that :: is a macro), so i definitely could not implement that functionality

lwhorton16:04:42

nevermind I found :: really quickly, seems to be a built-in to clojure namespaces

nidu16:04:54

:: expands into current namespace

nidu16:04:40

Very transient state can be placed in local ratom without relying on app-db of course.

lwhorton16:04:30

i figured as much, but there are some downsides to local ratoms… like the fact that a previously really dumb component (basically a fn that only understands hiccup syntax) now has knowledge of ratoms and @ derefs

lwhorton16:04:30

also, any time you need to do anything extra with an input field (on the fly validation, error correction, updating other parts of app) it needs to become global state anyhow.

nidu16:04:53

but shoudn't it know about subscriptions and handlers in other case?

nidu16:04:59

If you need to update other part then certainly it's simpler to be done via app-db. But not every validation requires app-db as far as i can imagine.

lwhorton16:04:05

i try to keep as many components “pure/dumb” as possible.. so often I have a parent that is responsible for handing down on-change anon functions that themselves do the dispatching. similar for any subscriptions or data sources. one parent component “knows” that it’s hooked up to reframe/reagent and subscribes, derefs, etc… but all its children can be completely ignorant of that fact.

nidu16:04:39

looks like a good approach to me simple_smile

lwhorton16:04:58

this allows for reuse at different places in the app without any changes to the component, only different parent implementations

lwhorton16:04:10

thanks for the :: tip though, that’s great

lwhorton18:04:31

has anyone settled around a convention for initializing a component or slice of the database with default data? currently I’m doing it inside the handlers, but it seems very cumbersome to have to have a (if (empty some-state-slice) (do init-data) (do the-real-manipulation-I-need-to-do)) for each and every handler 😕

lwhorton18:04:11

for example, I don’t want to have to write this for every handler if there’s a more clever way to avoid it:

(register-handler
  :some-event
  (fn [db [_]]
    (let [users (:users db)
          checkboxes (get-in db [:component :checkboxes])]
      ;; first time event is fired checkboxes isnt init
      (if (empty? checkboxes)
        (assoc-in db ...)
        (update-in db ...)))))

danielcompton20:04:26

@lwhorton: we initialise the whole db on startup

danielcompton20:04:07

alternatively, there must be some event which triggers a component to be shown/initialised, try putting it in there

mikethompson22:04:24

@lwhorton: Eeek. I'd never put that initialisation code in each handler. As Daniel says, and as is shown in the todomvc example, just put the initial data into app-db on startup via a (dispatch [:put-in-initial-data]) Also , regarding your point 2 above, (complex updates in event handlers), consider using https://github.com/LonoCloud/synthread To understand synthread, be sure to watch the video http://www.infoq.com/presentations/Macros-Monads. But, honestly, we only use it rarely, most of our event handlers are fairly simple.

mikethompson22:04:57

For what it is worth, we never use ::. Instead we use synthetic namespaces in our event ids. For example: :some-high-level-description/event-level

mikethompson22:04:33

Just because you use that keyword above, doesn't mean there actually has to be a cljs namespace called some-high-level-description

lwhorton23:04:44

Thanks Mike, I’ll take a look. I figured there was something wrong with that pattern. I’m building out the skeleton of an app and working on modules one at a time. I was trying to avoid having a giant state blob that I have to maintain at the absolute top level, and instead let a module manage its own slice.

mikethompson23:04:43

One other approach would be to use middleware

mikethompson23:04:54

For module X, all event handlers are wrapped in a piece of middleware that checks (in the before position) that the incoming data has been initialised. If it hasn't been, then it replaces the absent state with initial state and give that on to the handler (which will duly modify it) and that then becomes the new state.

mikethompson23:04:40

Keep middleware in mind whenever you have either "common" or "cross cutting" concerns which you'd like to factor out of event handlers

mikethompson23:04:00

In your situation, this initialisation falls into the "common" bucket (common actions across a certain set of event handlers).

mikethompson23:04:16

Now, even if you do this ... you'll still have to handle the absence of state in the subscription handlers. They will have to interpret "absence" in some sensible way, so the views which get this sensible interpretation, know there's nothing there, and don't fall in a screaming heap.

adamkowalski23:04:07

@mikethompson: hey in tutorial you say that A Middleware is an Endofunctor, and a collection of Middleware with clojure.core/comp is a Monoid.

adamkowalski23:04:14

Does that mean that they are also monads?

adamkowalski23:04:33

and they compose under a kleisli arrow

mikethompson23:04:00

I'm no expert on Category theory, but not as I understand it. Just because something is a Monoid, that doesn't mean it is a Monad.

mikethompson23:04:08

But honestly, when it comes to this sort of discussion, I generally hide under my blankets

mikethompson23:04:28

I know only enough to be dangerous

adamkowalski23:04:37

haha, i guess you and me are in the same boat

adamkowalski23:04:12

I learned the basics when I was trying to teach myself haskell but it seems like the general idea of middleware is that you take a handler and modify it returning a new handler

adamkowalski23:04:54

so it seems like you have a monad, where the context is a handler, and the values inside are what you able to manipulate as long as you end up returning the context again

adamkowalski23:04:58

sounds like bind

adamkowalski23:04:44

then you can chain the middleware together so the types are sort of like: a -> m b => b -> m c => c -> m d

adamkowalski23:04:54

so you end up with a -> m d

mikethompson23:04:26

When a function takes a and returns a, it is an Endofunctor. That part I'm fairly confident about simple_smile

mikethompson23:04:56

Beyond that clojure.core/comp seems, to me, to satisfy the associative binary operation necessary for the whole thing to be a Monoid

mikethompson23:04:24

But at this point, the blankets are already over my head and I'm humming calming tunes to myself.

adamkowalski23:04:07

yeah comp is nice but you end up with a slightly weird thing where you run the handlers from left to right even though you compose right to left

adamkowalski23:04:36

i think it could be possible to write a slightly different compose that would maintain the order if that sort of thing is desirable

adamkowalski23:04:52

anyway I guess I was just rambling about something that is probably unimportant. thanks for the clarification about them being endofuctors