DEV Community

Discussion on: Immutability - something worth striving for

Collapse
 
kspeakman profile image
Kasey Speakman • Edited

Thanks for the article!

I recently discovered a couple of interesting organizational tidbits for MVU projects that might be of interest to you.

For cases where you want to use MVU in a side-effectful language, I stumbled upon a strategy to isolate side effects. This allows init and update to stay pure/testable and just declare what side effects they need to run. Then a separate function is responsible for performing the declared side effects.

The other discovery for me was about the organization of MVU projects. When I started using Elm, it was espousing The Elm Architecture. Which basically packaged Msg/Model/View/Update for each small piece of the app. However, this quickly proved problematic as there are UI actions that sometimes cross different pieces. Such as switching pages in a SPA. This gave rise to an OutMsg pattern to send messages to different parts, but ultimately was a bit tedious to use. So the last advice I heard is "flat is better than nested". We did this and ended up with giant Msg types and giant update statements. It works fine, but it feels a bit disorganized, especially when you consider swathes of messages start with the same prefix: CourseSearchInput, CourseSearchSubmit, CourseSearchReset, etc. And having to Ctrl-F to find a specific one.

So it finally hit me that Model/View/Update actually has no problem being composed from pieces. It is Msgs that put a wrinkle in things, because any Msg might need to be sent from anywhere else. (Including from external effects.) So Msgs interact with but are independent of the MVU structure. (This also makes it more clear why Msg isn't part of the MVU acronym, which always bothered me.)

So with my latest project, I have started to go back to The Elm Architecture, but only for the MVU parts, not for Msg. All Msgs are defined first and every other part of the app can access all of them. Optionally, you can also choose to organize Msgs hierarchically instead of putting them in one big flat list. Since they are independently defined from the MVU parts, it is no longer a problem to do this.

Not sure if that helps, but I thought I would share what I learned. Cheers!

Collapse
 
rwoodnz profile image
Richard Wood

Hi Kasey,

My apology I didn't reply at the time as I wasn't across the OutMsg pattern and had less experience with the issues you describe. Now I re-read your comment and it makes sense to me.

I attended Elm Europe again this year and saw a couple of presentations where it came up, including an attempt to provide an actor approach to component structure and messages.

My concern with various attempts is when we get scaffolding code doing delegation and tricks that have no meaning otherwise in the application, making it an issue for future users of the code. The Elm language appears to be simply missing some useful structures built-in that would help componentise.

I agree with keeping all messages in one place. I recently did a project where I have them in the same file but broke them up by using a msg wrapper and msg types for each component. This works well for both bug finding and readability.

You can see my blog post about it here: Keeping it simple beyond a basic Elm app

Collapse
 
kspeakman profile image
Kasey Speakman • Edited

Thanks for the link to the article. We use some of those same patterns.

Lately, we have started to match both model and msg in our update function. Since messages are top level, you can technically receive a message which is not valid for the current view. This accounts for that case and also nicely unwraps the page-level model and msg all at once.

// F#, using Elmish
match model.Page, msg with
...
| CoursePage pageModel, CourseMsg pageMsg ->
    let nPageModel, effects = Course.update pageMsg pageModel
    { model with Page = CoursePage nPageModel }, effects
| CoursePage pageModel, badMsg ->
    // log?
    model, []

We handle page changes within the same structure.

match model.Page, msg with
| _, OpenCoursePage courseId ->
    let nPageModel, effects = Course.init courseId
    { model with Page = CoursePage nPageModel }, effects
...

This works with navigation. For example, it is pretty easy to parse the URL /course/abc123 into OpenCoursePage "abc123". We ended up with too many OpenXX messages, so they were organized under a wrapper message tag like OpenPage .... Then a central function is responsible for calling the correct page init.

Since these are recurring patterns, I've tried to parameterize it different ways. But that requires some extra abstraction/scaffolding. And then that abstraction must be learned by other devs (including Future Me when I've been away from the code for 6 months). The team ultimately deemed it not worth it compared to an extra line or 2 of very clear (and copy/paste-able) boilerplate.

We will continue to try different things and refine how we organize our MVU code. But this is where we are for now. We have used it for top-level pages and for 2nd level sub-pages.