Perils of accidental complexity in re-frame

Many people have praised re-frame, with good reason: it can really help in developing rich browser apps, by providing a principled approach to state management (and documenting it extremely well).

I’d like to do something different here: discussing how the design of re-frame can encourage some forms of accidental complexity, and how to mitigate them.

Let me be very explicit: this is not another “is re-frame good?” discussion.

Here are some ‘threats’ of complexity which I’ve identified:

Proliferation of names. In addition to UI components, re-frame makes you name subscriptions and state transitions. As is well articulated in Elements of Clojure, naming too many things is an often-overlooked cause of complexity: choosing good names is difficult, and we should avoid it when we can.

Over-specificity. It’s not very straightforward to make generic UI components or state management logic in re-frame.

AFAICT, one reason for that is that we are naturally inclined to hard-code specific app-db paths in components and subscriptions: one mitigation strategy is to turn these paths to parameters.

Another potential reason for that issue is the following:

Lack of expressiveness of data-oriented state management. Subscriptions and state transitions are expressed in a data-oriented language, which more predictable but less expressive than Clojure: less expressiveness can result in less composability, less reusability, and less abstraction power.

For example, I’ve often wished for (and guiltily implemented) an :re-frame.effect/update-in handler, which you call with an anonymous function.

The way I see it, one mitigation for that is too just give up on some of the data-orientation, as in the above example: parameterize your subscriptions and effects with non-data objects such as functions. You will use some transparency / predictability benefits of your effect, but the result is that it prevents the implementation of 10 effects that all do almost the same thing, it’s probably a win.

Global environmental coupling. The app-db in re-frame is a global singleton. This can make it hard to implement components with isolated effects, e.g for testing or pre-rendering.

Conclusion

The forms of complexity that re-frame combats are well-known: re-frame encourages an approach to state management that is very functional, centralized, and data-oriented, through guidelines that are easy to follow.

Several of the pitfalls I’ve identified (“proliferation of names”, “over-specificity”, “lack of expressiveness”) might sound familiar: they are well-known issues with class-based programming languages.

And indeed, as had happened with class-based languages, I fear that re-frame users who embrace its principles too readily might succumb to unanticipated forms of accidental complexity, through what I’d call the "fallacy of ceremony": the illusion that, because you’re continuously using constructs that stem from a principled design, you are necessarily writing a well-designed program.

The way I see it, that’s the fault of the user more than that of the tool, and the solution is to keep an alert and critical eye for these complexity pitfalls… and occasionally make the required transgression to re-frame’s principles. I hope such discussions can improve the awareness of such pitfalls.

What do you think? What have I missed?

20 Likes

In four years of re-frame usage I basically hit all of your points. I don’t have ‘a solution’.

In my latest project, I added an api.cljs file to each namespace (next to ‘events’ and ‘subs’). It can help reduce complexity somewhat, or at least motivate you to get rid of paths. A bit like what @ericnormand advocates here, ie. to build an functional API next to the event handlers, which then call those functions.

It is a delicate dance. It’s a balancing act on the amount of re-usability of components and moving complexity around.
In general I try to keep the views simple, and don’t focus too much on re-usability of them, unless it’s like a real generic component which I tend to use as building block in various spots in the UI (ie, some rich text editor that’s used throughout the application).

Then in my wrapping components, I hardcode the ‘API’ more explicitely., ie. :on-change #(rf/dispatch [:order.comments/update-comment %], but the generic component itself just gets a callback passed in, and might even work with a local atom, bypassing re-frame altogether.

The fallacy of ceremony can show up when you don’t remember the keys you’ve named your re-frame handlers and subscriptions.

For me, there’s not one compass to sail by, it depends a lot, and I mis-step occasionally. But on balance, it is definitely the way to do UI in applications imho. I love it tbh :slight_smile:

2 Likes

In the latest ClojureScript podcast episodes Tony Kay discussed some trade-offs between an event-based system like Re-frame and a pull-based/declarative system (not sure if I used the correct term) like Om.next and Fulcro. Might be worth a listen.

5 Likes

https://github.com/roman01la/uix is proving a good alternative to re-frame wrt. making standalone components in my experience, esp since it also brings a la carte global state/subs with its adapton implementation. I wouldn’t rush back to re-frame, having used it for a few months now.

See also: https://www.youtube.com/playlist?list=PLHOTezm7WWkl4rfd6j0fsL65DrxfWz4XM

2 Likes

This pretty much matches my experience with re-frame.

1 Like

“Design patterns are missing language features”

Reframe is a half-baked action interpreter. The problem is Clojure doesn’t have a good way to define interpreters/evaluators. This is all terribly ironic given Clojure is a Lisp, and that the last 15 years of Haskell research have pretty much nailed composable interpreters.

“Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp”

Interesting. Any pointers to readings about this? Haskell stuff, etc. Also curious where Clojure is lacking in this regard.

One thing I see often in Clojurescript UIs is direct mutation of state inside event handlers. This is horrible thing to do! It creates strong coupling between the app logic and the presentation logic, and it spreads app logic all over the code in an untestable mess.
A better way is to separate events from actions. Actions are pure functions of state and a message (which includes information from the event). UI code should only be concerned with UI matters, so event handlers should only send a message about the event and any local state. Then such messages are mapped to actions via a runtime-modifyable map, so that all state-updating code can be written as normal, testable, pure functions of app state and a message.

1 Like

IMO, this statement of caution is overly general. Oftentimes, state is really only about presentation, and trying to decouple them only creates complexity and tedium. This is similar to the myth that “HTML is about content and CSS is about style, so they should be kept separate” - when in fact partitioning the code by components is much more sound.

I think we need to be more precise about what kind of state we deal with, e.g “information state” vs “presentation state”.

10 Likes

IMO the UI should be only a reflection the app state and nothing else, i.e. the UI is a presentation of the information, or a projection of the app state onto the screen. Everything that happens on the screen should be a visualization of the app state. It’s basic model and view. No statefull UI components at all.

model → view → messages → model

Everything that happens on the screen should be a visualization of the app state

YMMV but I find having animation states, text/mouse cursor positions, currently focused element, etc. being a part of application state very confusing.

4 Likes

all of those except animation states go well in application state because then you get global app undo/redo! Including mouse position - it works. I’ve used this undo/redo as a presentation tool – record everything! You can also persist the entire history of states, every user interaction and replay it later.

Yes but when you throw in reusability, often you cannot use subs and events as effectively anymore. Basically you add a need to pass in component ID’s etc.

Also you dont say what message a UI should send.

There’s tons of options. Event can be generic, then all kinds of params need to be passed in the view. Wow reusable! Yeah but at a price of more complexity in the view.

It can be more app-specific, ie ::cart/add-product, and with a subscription on ::cart/selected-products

But now it’s probably not a reusable component anymore.

This might or might not be a problem, what i am basically saying is, there is no ONE way, it depends very much what the situation is.

I have had a ‘reusable’ mail widget, a ‘generic’ popup for creating emails. All the dispatch actions were passed in. Just for the sake of DRY view code. Because i needed it at 3 places

I quickly realized this was the wrong way; when things are more generic, you probably going to want local reagent atoms more, and the output sent out through normal callbacks.

Wrapping components can then be fairly thin and call business actions.

Again my point is: choices…

1 Like

There’s some more succesful ways of generic reusable components though, which worked for me, most notably a reframe based html table component with sorting which works well in practice, and is available as a lib. Same story for a drag/drop functionality.

Not perfect; but it shows up in multiple production apps for me, and works fairly well.

See Re-datagrid and re-dnd