Hi Elm community,
This is not an issue, just a discussion I would like to have on building modular, well-defined applications under the elm architecture. I will try to keep it brief, which will require a lot of hand-waving.
The most important pieces of any application architecture I have implemented always seem to turn out the same:
- modularity / re-use with good separation of concerns
- the ability to clearly and predictably communicate between components
- the ability to concisely and predictably coordinate multiple components to perform major context switches / transitions.
These three pieces enormously impact the refactor-ability of the codebase, the velocity of developers in that codebase, and the lifespan/maintainability of the entire application.
Modularity / Re-use
Ideally an entire application is built from the bottom up from small, loosely coupled, reusable components. If these components can be completely unaware of their place in an application, and only serve a small, defined purpose with a minimal api surface, you are on the right track to a highly maintainable and refactor-able code base.
Clear and predictable communication
If each component is modular, you can build up a tree hierarchy of components where communication between depths or leaves or branches happens only across a small interface at defined intersections. No matter the form of communication - be it a flux architecture, a messaging bus, or pub/sub, this "bounded" communication also keeps you on the right track to a highly maintainable and refactor-able code base.
The ability to coordinate context switches
Most apps have major contextual chunks that form concrete or logical boundaries in which to partition your components. Sometimes this is as simple as main navigation tabs, and sometimes it is more nuanced (consider following a deep link to a different part of your application). If your architecture does not offer a way to reliably and conveniently swap between these contexts, you are going to end up with a lot of fragile code that has to be hand-held through state re-hydration, serialization, branching within contexts, and all sorts of other tedious and error-prone bandaids.
Where does elm fit in?
I have been fooling around with the elm architecture for a little bit, and have tried to familiarize myself with its strengths and weaknesses. It seems relatively similar to the redux pattern (immutable, explicit top->bottom action flow, predictability) with perhaps the major difference being that truly reusable components are much simpler.
Modularity
Using Signals
and forwardTo
, parents provide their reusable children the information necessary to "just work" so that children can be completely unaware of the rest of the application. There are plenty of (mostly) clear examples where this can be extended to allow lists of lists of entirely dynamic reusable components (the "Model with a context" example is still pretty unclear to me).
One issue I bumped into almost immediately using this pattern is a similar but slightly more advanced use case: when a parent does not merely want to forward its child's actions, but is interested in a subset of those actions.
For example, consider a simple chat interface with a MessageBox
, a Post
, and a Chat
component to hold everything together. What I want to be able to do is pipe InputChanged
into the MessageBox
without needing to know the details, but when MessageSent
appears, capture some details about that action inside of the parent. A rough sketch would be something like...
type Action
= MessageBoxAction MessageBox.Action
update : Action -> Model -> Model
update act model =
case act of
MessageBoxAction msg ->
case msg of
-- for a particular action, do something before forwarding it on to the child
MessageBox.Action.MessageSent ->
let
updatedMessage = MessageBox.update msg model.currentMessage
newPost = Post.init msg model.posts
in
{ model |
posts = model.posts :: [ newPost ]
, currentMessage = updatedMessage
}
-- for any unspecified actions, just forward to the child
_ ->
{ model |
currentMessage = MessageBox.update nested model.currentMessage
}
This won't compile. I can think of a few other ways to achieve the same end, but none of them are a good idea because they either burden the reusable component with unnecessary requirements or end up with circular and difficult-to-trace "on update do another update" patterns.
I feel like if a cleaner general pattern can be reached (and it might be simple and I'm just missing it), then truly reusable components are actually possible in elm.
Communication
This is pretty inherit in elm. Everything is explicit, and the communication lines are well drawn down a component hierarchy. The biggest challenge I believe will be maintaining a small API surface across components. Sometimes the temptation is strong to update on actions that probably aren't any of your component's business. This eventually carves out an application where changing one action name, or removing a single component can break everything in that hierarchy subtree.
root
a b
c d e f g
h i j k l
"Update H" needs to be piped from a -> c -> h. In a naive implementation, changing the action to "Update h" requires also changing a and c. I think elm avoids this through the use of a generic "On some child action just pipe the message through and update the model".
Context
I have no idea how elm might handle something like this. I believe the excellent work done over at redux-saga handles context switching in one of the most elegant ways I have seen. Because redux, like the elm architecture, is basically an event-sourced architecture, the concept of sagas "just works". When SOME_TRANSITIONING_EVENT
arrives, you can utilize the api of components to predictably transition the application step by step.
The problem with sagas, though, is they are by-nature imperative and not functional. It would be great to hear some thoughts on how the elm architecture handles this case.