Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi page applicatiion #144

Closed
poborin opened this issue Jul 27, 2018 · 29 comments
Closed

Multi page applicatiion #144

poborin opened this issue Jul 27, 2018 · 29 comments
Labels
s/needs infos The issue needs to be completed

Comments

@poborin
Copy link

poborin commented Jul 27, 2018

I'm trying understand how a multi page application is going to look.

Logically, a multi page application should be split on files where each file represents a page or a subset of the application. However, in the AllControls sample I've seen that the navigation stack and all pages are maintained in the same file.

It makes me wonder, how will a complex application will look like? Will it have only one file with init, update and view and ViewElements will be split on modules, or it will have a few modules with init, update and view?

@TimLariviere
Copy link
Member

In my opinion, each page should have its own module with its Msg, Model, init, update and view.
It's way more easier to maintain than putting the view part somewhere and the update part in an extra large file.

But this requires some extra work.

Thanks to @dsyme's help on Slack, I've made a small sample that splits one page per file instead of everything the App module.

If you want to take a look: https://github.com/TimLariviere/ElmishContacts

It's certainly not the most optimized example, and I wonder if I can remove some code.
But basically, each page will have own domain + an ExternalMsg which will carry the page's intent to do something on another page. (like navigation).

You can see it in action here:
https://github.com/TimLariviere/ElmishContacts/blob/master/ElmishContacts/MainPage.fs#L46
https://github.com/TimLariviere/ElmishContacts/blob/master/ElmishContacts/App.fs#L44

The downside compared to one mega file is that it requires a lot more messages to make a multi-page action.
From the example above:

  • 1 single App file : [Button click] -> SelectContact -> [Update model to reflect it and display a new page]
  • In separate files: [Button click] -> Page Internal Msg SelectContact -> Page External Msg ContactSelected -> App Msg GoToItem -> Item Page init

@xperiandri
Copy link

Could you make the same sample with XAML pages?

@TimLariviere
Copy link
Member

TimLariviere commented Jul 27, 2018

I might give it a shot in the next few days.
Curious at how easy it would be to convert to half elmish.

@dsyme
Copy link
Collaborator

dsyme commented Jul 27, 2018

@xperiandri There was a multi-page half-elmish sample implementing a master-detail app but I removed it, see https://github.com/fsprojects/Elmish.XamarinForms/tree/d1f3a833bde59983cc671eab4b8d781c0dcb3307/Samples/StaticXaml/MasterDetailApp.

As mentioned on another thread I wasn't happy about the composition model View.combo etc, which was removed at the same time, and the app seemed so clumsy compared to the full-elmish samples. But the basic code should still run if you resurrect the composition model too or replace it.

@xperiandri
Copy link

So what is your point of XAML-elmish? Will you introduce something to make it less clumsy?

@xperiandri
Copy link

Because defining UI in code is definitely not what I ever considered to do.

@dsyme
Copy link
Collaborator

dsyme commented Jul 27, 2018

EXF is primarily a code-oriented framework oriented around full-elmish, with an added half-elmish Xaml option. In full-elmish the correspondence between the View.* DSL and Xaml is more or less direct so it's possible to think of it as Xaml-designer code with a different syntax and less complexity.

We'd definitely accept further samples under "Samples/StaticView" and any contributions to make half-elmish easier.

@dsyme dsyme added the s/needs infos The issue needs to be completed label Jul 31, 2018
@aspnetde
Copy link

aspnetde commented May 1, 2019

I am just digging through the docs, issues, and the samples to get an idea on how all this is intended to work. I ended up with the same question as raised here as I spent the last couple of years working on a fairly large Xamarin.iOS project with in the end roughly ~100 dialogs (-> 100 controllers + 100 corresponding view models).

I can't imagine handling an application of that size in one big app.fs. The ElmishContacts sample helps as it provides an approach to split it up somehow. But if I understand it right there's still one big integration point here.

This thread is almost one year old, so:

  1. Have there been other attempts trying to build multi-page apps with different approaches?
  2. How is the current "official" position on how to do it "right"?
  3. Am I right in my assumption that Half Elmish would solve that problem as navigation works the usual Xamarin.Forms way there?

@TimLariviere
Copy link
Member

I can't imagine handling an application of that size in one big app.fs. The ElmishContacts sample helps as it provides an approach to split it up somehow. But if I understand it right there's still one big integration point here.

Fabulous (and any Elm-like framework I think) relies on a unique central processing loop, and as far as I know there's no way around that.
So one easy way to avoid having one big App.fs file is to break things into smaller things (I chose to break per page in ElmishContacts) and compose them back together.
ElmishContacts is quite small, so that relatively easy to keep it "horizontally" splitted (I mean by that there's the root App.fs and each page is a child of it).

For bigger projects like the one you mention, maybe one way to ease this is to use a "vertical split".
Regroup several pages into their own integration point (logical grouping), and then compose those integration points back in the App.fs file.
Repeat this nesting as much as needed.

From my experience with ElmishContacts, this shouldn't be too complicated.
The only issue I encountered was cross-page messages, which I solved with the ExternalMsg trick.
Should do fine even with a multi-level nesting. If the message stays in the same logical group, the parent won't even need to know about it.

Hypothetically, you could also split your Fabulous app into several Fabulous apps (like the logical groups).
Xamarin.Forms needs one native page (Xamarin.iOS Controller) to boot a XF app, so does Fabulous.
So you could in theory navigate away from the first native page to another one (inside the native project), and boot another instance of Fabulous.
Not sure this is a viable solution though...

How is the current "official" position on how to do it "right"?

I didn't saw other Fabulous apps trying another approach. Mostly due to their size.
And as I took a lot of inspiration from the Elmish samples for ElmishContacts, I would say the official position on how to do it right is by composing the pages.
Maybe someone else may have a different opinion.

Am I right in my assumption that Half Elmish would solve that problem as navigation works the usual Xamarin.Forms way there?

I don't think so.
You would still have one unique Fabulous app, with this one central processing loop.
So even if you manage navigation through the XF exposed methods, you would still need to notify Fabulous to update the state.

@aspnetde
Copy link

aspnetde commented May 1, 2019

Thanks for your feedback!

Xamarin.Forms needs one native page (Xamarin.iOS Controller) to boot a XF app, so does Fabulous.
So you could in theory navigate away from the first native page to another one (inside the native project), and boot another instance of Fabulous.
Not sure this is a viable solution though...

What you (I) usually do is to replace the RootViewController (iOS) / the application's MainPage (XF).

A classic example even for simple apps would be a login page for all cases in which the user isn't authenticated and a let's say start page for all other cases.

Once the user signed in you probably don't want to have the login page around in your navigation stack as it would most likely never see daylight. So you make a decision at startup on whether to set the login page or the start page as your application's MainPage. And after logging in/out you replace that page again with the one that represents the state change (logged in/out).

I guess that could somehow happen/be managed by the "main Fabulous app", the root node so to speak.

You would still have one unique Fabulous app, with this one central processing loop.
So even if you manage navigation through the XF exposed methods, you would still need to notify Fabulous to update the state.

Random thoughts:

Let's say I have a page like that. It leverages MVU based on the Half Elmish functionality of Fabulous and it could theoretically be instantiated without the small ceremony in here. This would leave two questions: 1) How to navigate, and 2) how to manage (load/save) the state.

Navigation could be done by XF's navigation model. Easy. And State management would be possible in various ways (reading from/writing to a database, using a Redux store etc.). So at the end of the day it would leave us with pages that leverage MVU for themselves but are not coupled together in one big application.

I can't tell right now if this would be a good idea or a step backwards, though :-).

@aspnetde
Copy link

aspnetde commented May 2, 2019

Turns out @Zaid-Ajaj already dealt with it:

Update: There's also a helpful comment by @cmeeren there linking to some reddit threads and this one.

@cmeeren
Copy link

cmeeren commented May 2, 2019

Yes, I watched the talk by @Zaid-Ajaj with great interest, but he seems to advocate patterns that are somewhat opposite to other clear Elm/TEA/MVU scaling guidelines. Componentization can, as I understand it, lead to a lot of boilerplate, duplicated state with possibility for bugs, etc.

For the record, here are the links from my comment to that video:

https://www.reddit.com/r/elm/comments/5xdl9z/elm_architecture_with_a_reduxlike_store_pattern/

https://www.reddit.com/r/elm/comments/5jd2xn/how_to_structure_elm_with_multiple_models/dbuu0m4/

https://www.reddit.com/r/elm/comments/65s0g4/resources_regarding_scaling_elm_apps/

https://dev.to/elmupdate/summarising-elm-scaling-strategy-1bjn

@aspnetde
Copy link

aspnetde commented May 4, 2019

You would still have one unique Fabulous app, with this one central processing loop.
So even if you manage navigation through the XF exposed methods, you would still need to notify Fabulous to update the state.

I gave it a quick try. Result: It's already possible to host one program per page. See this sample where from the Main Page you can navigate to Page 2 which is actually running the same stuff with the whole MVU cycle for itself.

It's of course kind of dirty and not at all how the Elm stuff is intended to work with in the first place I guess. On the other hand it brings the core idea of MVU to life and deals with the main trade-off with the current state of Fabulous: Having a new abstraction layer on top of another abstraction layer that took years to become as stable and powerful as it is today: XAML (for XF).

@Zaid-Ajaj
Copy link

@cmeeren You seem to have missed the main point of my talk, I was not advocating one certain way of doing things (componentization) but rather introducing one way of scaling. As I mentioned in the very beginning of the talk, the topic poorly documented and commands were seen as black boxes so I spent a great deal of the talk explaining commands and their relation to the dispatch loop etc. because people were starting to build apps without thinking too much about these concerns. Let's say that my talk was an intro into the topic, not a "this is how you should do it" kind of thing.

As for going "opposite to other clear Elm/TEA/MVU scaling guidelines", I have to disagree. In the functional world, there are too many way to come up with abstractions to break things down and componentization in Elm(ish) is just one way of doing so on component level. The argument that it eventually leads to boilerplate code, literally doesn't mean anything without context because it all depends on the complexity of the application being written: the interactions between pieces of user interface and the significance of said boilerplate with respect to the amount of the overall code. For example, if your components don't need to talk to each other, then the cost of boilerplate you need to write is affordable compared to gains of componentization. I find components easy, not just to think about but to explain to audience of entry-level Elmish which is again a big deal in teams adopting the paradigm as they make the mental jump from modelling one page to multiple pages and connecting them together in DIY fashion (people are a very important factor, think why vue.js and golang are so popular). In conclusion, componentization in Elmish is not the silver bullet but can still be successfully applied to many many user interface applications.

To make a long story short, this kind of discussion really comes down to purist vs pragmatist perspective. I very much belong to the latter category which is why I don't mind a bit of boilerplate code in my apps. Hence I would still use this technique where appropriate.

@cmeeren
Copy link

cmeeren commented May 5, 2019

Thanks for the feedback, @Zaid-Ajaj!

Disclaimer: All of the below should be taken as strong opinions, weakly held. I'm interested in learning and growing, both personally and for us as a community. And none of this is to be taken personally. :)


For example, if your components don't need to talk to each other, then the cost of boilerplate you need to write is affordable compared to gains of componentization.

Absolutely, which I also mention in the comment. If the components do not need to communicate or don't share state, components are great!

My point was just that in cases where components do need to talk to each other or know about each other's state, such as in your example (around 47:00 as mentioned in the comment, where the child components needed access to the parent state, which then had to be duplicated, and thus updated several places), you not only get unnecessary boilerplate (updating in several places), but also unnecessary possibility for bugs (not properly synchronizing state). The reason I say "unnecessary" is because, as described in the sources I link to, there is an alternative way to split up your functions that do not impart any unneeded boilerplate or requirement to synchronize state across components: Instead of splitting up into full MVU components, simply split your M by itself, V by itself, and U by itself by delegating to helper functions (V/U) or composing types (M).

The only drawback to this is that you don't have multiple smaller MVU components that can be reasoned about in isolation in their full MVU lifecycle. But that's not possible anyway if the child component must communicate with its parent or know about its state (which is what your example shows). If a child component really is separate from the parent (a rule of thumb could be that it must be reusable across domains), then yes, I agree that it would likely be better to have a separate MVU component. :)


I find components easy, not just to think about but to explain to audience of entry-level Elmish which is again a big deal in teams adopting the paradigm as they make the mental jump from modelling one page to multiple pages and connecting them together in DIY fashion (people are a very important factor, think why vue.js and golang are so popular).

Let me be clear that I think you're doing an excellent job at that. :) Rock on!

Now, I agree that components are easy to reason about, but as said, only insofar as they can be understood in isolation. The reason I wrote the comment (my first YouTube comment, I think!) is exactly because, based on the discussions and tips in the links I provided, components are not in fact the simplest solution if they depend on each other. I can vividly imagine new users being annoyed at the MVU pattern because they are factoring out into child components and need to duplicate state in the child component, and get bugs because of missing state synchronization (and the boilerplate needed to update the state all necessary places).

How to factor out stuff when scaling is a common point of confusion among those new to MVU, because as programmers we're so very used to thinking in terms of components, but in this case (if the components are coupled by communicating upwards or sharing state), very little good comes out of it. As Richard Feldman says:

At this point I've heard enough horror stories to conclude that thinking in terms of "parent," "child," and "component" (smart, dumb, or otherwise!) is a mindset that has no obvious drawbacks early on, but leads to a worse and worse experience as the code base scales.

In contrast, thinking only in terms of "caller" tends to yield better results. "What information do I need the caller to pass me, and what information should I return to the caller?" are great questions to ask. This simple question leads you to focus on scaling one function at a time, in isolation, which is key to avoiding accidental complexity. (The "component" mindset turns out to be a magnet for accidental complexity in Elm.)

(If you haven't yet read the links I posted, I highly recommend checking them out!)


To make a long story short, this kind of discussion really comes down to purist vs pragmatist perspective. I very much belong to the latter category which is why I don't mind a bit of boilerplate code in my apps. Hence I would still use this technique where appropriate.

I see what you're getting at, but I don't think it has anything to do with purity in this particular case. My point is that componentization (again, unless the components are actually independent) often gives unnecessary boilerplate and introduces chance of bugs in state synchronization. On the other hand, splitting M, V and U by themselves does not lead to unnecessary boilerplate or state duplication, and IMHO are in fact easier to understand than coupled components.

In my mind then (unless the components are independent), splitting M, V, and U by themselves is the pragmatic choice. :)

@aspnetde
Copy link

aspnetde commented May 5, 2019

@cmeeren @Zaid-Ajaj Thanks for the discussion :)

Let me come back to Fabulous here and instead of thinking of abstract components as widgets or something like that let's talk about Xamarin Forms Pages.

Let's say we have 3 of them:

  1. A list of contacts.
  2. A contact detail.
  3. An edit form for a specific contact.

I have a (very) hard time seeing all the local concerns of those 3 pages being represented in the global state. I do see the advantages as time-travel debugging etc. but mixing global and local concerns ... well, concerns me.

For example while globally only a list of all contacts may be important, each of those pages has different local stuff to deal with (e.g. validation for 3.).

Interestingly I had to deal with that (as probably any of us) in the past. One of my last approaches, which turned out to be quite successful for the team, has been using MVVM + Redux (sample, blog post).

This works as follows: All 3 pages subscribe to the information held in in the global Redux Store that is of particular interest for them. While 1) may get notified when any contact changes, 2) may only update itself when the displayed contact has been updated. Besides that all of the pages are able to keep local state (within the view model).

I can't help but to think of combining that global state (redux style) with MVU at the local level. 🤔 😀

@cmeeren
Copy link

cmeeren commented May 5, 2019

I have a (very) hard time seeing all the local concerns of those 3 pages being represented in the global state. I do see the advantages as time-travel debugging etc. but mixing global and local concerns ... well, concerns me.

An important question is, why? Is there a sound basis for that concern, or is it just a felling? Let's explore that for a bit.

I do get where you're coming from as I've had the same thoughts myself. Interestingly I've also developed an MVVM + Redux solution for an app I have in production at work. I briefly mention it in this issue: elmish/Elmish.WPF#40 (search for "The way I have solved this"). It sounds like we have more or less exactly the same architecture, because your description perfectly describes my approach, too. The reason for the Redux/MVVM hybrid was that Fabulous/Elmish.XF didn't exist when I created the app, and MVU requires highly nontrivial UI rendering support. The homegrown hybrid works well, but it's far more complex than MVU would have been.

As programmers we have a very strong urge to factor out whatever feels like separate concerns. A part of me would like to have the global app model be just the truly global stuff, such as global domain objects (contacts in your example). But I've come to accept that the model in MVU is not intended to be a just a database of global domain objects and other global state. One of the main strengths of MVU is its simplicity, stemming from its unidirectional flow in a single loop. This however requires that everything you need to do business logic and build the view should be part of the model. In your example, that includes raw input so that you can validate it and display error messages.

The benefit you get from MVU is fewer concepts and moving parts to keep in mind.

Now, as the links I posted previously mention, you can easily split M, V, and U by themselves. So you don't need to have all text field values directly at the root of your model. You can create a separate type for the contact edit form, containing the input values and other relevant stuff, and simply have this type as a member of your main state. You can also have a separate "update" function that updates just this member, if that branch of the main update function becomes too complex. (This helper would accept as params all the global stuff it needs too, e.g. the contact list.)

To hammer the point that the MVU model at first seems somewhat counter-intuitive when it comes to componentization/factoring, and because I think it might answer your question, allow me to highlight some quotes from two of the links I posted:

First: Elm Architecture with a Redux-like store pattern

Question:

One huge challenge for me has been that in standard implementations of the Elm Architecture (TEA) the state is centralized, but child components can only access the state they define for themselves.

This seems great for smaller applications, but while building apps with React/Redux, it's incredibly powerful to be able to pull in portions of the global state just about anywhere in the app. This allows for a nice decoupling of UI and Global state (e.g. normalized entities from an API, logged in user, etc.).

With React/Redux, children of the main app are divided into "smart containers" and "dumb components", with containers connecting to the global state while components are generally for rendering UI and passing messages back to the containers.

Reply by Richard Feldman:

We have 96,000 lines of Elm in production, and I am happy to report that you don't need this in Elm. :)

[...]

The OP is a familiar story to me, because I see variations on it pretty often. A lot of beginners come in with this mindset, and end up frustrated after they've begun to scale an application using familiar techniques from React (and/or Redux) and being surprised that the result was painful to work with.

At this point I've heard enough horror stories to conclude that thinking in terms of "parent," "child," and "component" (smart, dumb, or otherwise!) is a mindset that has no obvious drawbacks early on, but leads to a worse and worse experience as the code base scales.

Then from another thread: How to structure Elm with multiple models?

Question:

I am trying to get elm to work with elixir/phoenix. I understand the regular structure of elm MUV, but i find my self having a hard time understand how i would get elm to work with multiple pages, and also if I have nav bar that would contains msg how would I include all these functions together(e.g: dropdown and such)? All the tutorial that I have done seem to cover only SPA. If have multiple models how would i connect them all together to the main?(e.g a model that contain my nav function, plus a model that holds my data and such. )

Reply by Richard Feldman:

In Elm, thinking in terms of "components" is a common and totally understandable mistake. :)

Elm intentionally does not have a component system, because that idea is counterproductive in Elm. The official guide makes this explicit, and talks about what to do instead of thinking in terms of components: [link to now non-existent part of elm guide]

And in a later comment:

If I think "here is a part of my UI that is logically separate from other parts, so I'll give it its own Model, Msg, and update" then I am either (worst case) making my code more complex than it needs to be, or (best case) making it exactly as complex as I would have if I'd never been thinking in terms of "components." There's no scenario where I come out ahead.

The OP then gets straight to the heart of the matter:

If you say that sometimes its a bad idea to introduce a new "component" with it's own Msg, Model, Update etc. is it then a common theme in elm applications to have really large update functions that transform a large state? Or how can I split up my update logic in a way so that it's somehow logically decoupled?

Should I really mostly care about separating parts of my UI by introducing different view functions and helpers and have one (more or less) global model and update?

To which Richard has this excellent answer which is highly worth reading in its entirety for all its concrete advice on splitting up M, V, and U by themselves. The core of the argument boils down to:

These are the techniques I recommend using for dealing with problems of "____ is too big and I want to split it up." The key again is to treat one symptom at a time.

(Emphasis his.)

In summary then, my advice, based on my own limited experience and basically everything well-founded I've read about the matter, is this: Just start developing with everything in a single model, view, and update, and if things get too big (not just "feels" too big, but actually causes you pain), treat one symptom at a time by splitting one thing at a time.

Pain-driven development seems to be a good rule of thumb in the MVU world. As described, premature componentization is a root of many evils. :)

@aspnetde
Copy link

aspnetde commented May 5, 2019

Thanks for your feedback, very much appreciated!

I've already read Richard Feldman's answers you linked to before, and I've also seen his talk on the topic. I think we must be careful not to follow a cargo cult here, but that may just be my impression ;-).

In general I don't (yet) think that the Redux/MVVM approach I (probably both of us) took is more complex than TEA. At least my team had no problems working with it, despite being new to the architecture and F# at the same time. It obviously focuses only on the "MU part" and leaves the view out. But I caught myself already a couple of times thinking that something MVU does is strange or complex, but in fact isn't very much different from what I have been implementing with Redux after giving it a second thought.

Pain-driven development seems to be a good rule of thumb in the MVU world. As described, premature componentization is a root of many evils. :)

It feels a bit like watching one of my kids going to touch a hotplate... could be an induction hotplate, though ;-).

Thanks. I will give it a try and make my own experiments and decisions.

@cmeeren
Copy link

cmeeren commented May 5, 2019

I think we must be careful not to follow a cargo cult here, but that may just be my impression ;-).

Yes, one should always bring one's own best judgements to bear. Personally I'm always wary of the limitations of my own experience and very willing to listen to other's lived experiences. Richard says he has 100k lines of Elm code in production and can report that the MVU pattern in its purest form (Elm is pure, after all) works very well at that scale. He describes in concrete and detailed terms how one can solve scaling problems in ways that are consistent both internally, with the MVU pattern as a whole, and with my own experiences and reasoning. So I'm not afraid of a cult in this particular context. (Should my own future experiences indicate differently, I'll just have to reconcile that to the best of my ability when the time comes.)

That said, every project is different and there may very well be some unique constraints - technical or otherwise - that calls for a solution that deviates slightly or markedly from accepted norms. Furthermore, each developer is different, and a custom architecture might make more sense to some than "idiomatic" MVU - or simply require less investment in learning, meaning there can always be valid business (or personal) reasons to use A instead of B.

It feels a bit like watching one of my kids going to touch a hotplate... could be an induction hotplate, though ;-).

Thanks. I will give it a try and make my own experiments and decisions.

Yes, the proof is in the pudding, so the only way to be sure is to try different things for oneself, making notes of the pain points of each strategy, and always being critical of whether the pain is purely emotional (because it feels unusual) or is based in real problems. (For example, I'd guess emotional pain due to a pattern feeling unusual is the source of a lot of poor FP code from experienced programmers that are new to FP.)

Do let us know of your experience! :)

@Zaid-Ajaj
Copy link

This is probably not the best place for the discussion but I am glad I can discuss it anyways, lately I am have been thinking a lot about how to approach teaching the concepts of MVU in the best way possible (given my very humble experience with it, no 100K lines app yet 😄)

@cmeeren

My point was just that in cases where components do need to talk to each other or know about each other's state, such as in your example (around 47:00 as mentioned in the comment, where the child components needed access to the parent state, which then had to be duplicated, and thus updated several places), you not only get unnecessary boilerplate (updating in several places), but also unnecessary possibility for bugs (not properly synchronizing state)

That's a very good point, I am totally with you on this one, just let me say that around 47:00 I was already out of time to fully explain my discontent for the drawbacks of the approach. I remember noting that one "should be careful with components when syncing the data" but that's obviously not enough. I wanted to explain that when using this approach, people will start abusing Cmd.ofMsg instead of just re-using functions. (Cmd.ofMsg is dangerous because you can make the update function implicitly recursive and end up with cycle update graphs)

Now, I agree that components are easy to reason about, but as said, only insofar as they can be understood in isolation. The reason I wrote the comment (my first YouTube comment, I think!) is exactly because, based on the discussions and tips in the links I provided, components are not in fact the simplest solution if they depend on each other. I can vividly imagine new users being annoyed at the MVU pattern because they are factoring out into child components and need to duplicate state in the child component, and get bugs because of missing state synchronization (and the boilerplate needed to update the state all necessary places).

That's the thing, In my mind I think about components as if they were mechanical constructs, not much thinking is involved and the algorithm is simple, just implement a smaller MVU app and "connect" it later. But this "connect" part is not as straightforward as one might think once the components start talking to each other. On the other hand, using just functions is probably the best way to go but it stays "open ended" and vague as to where you should apply your refactorings and points of integration, there are no obvious rules to "just use functions" without making the code more complex than it should be (not implying that this is a problem per se). Lately, I have been narrowing down all my guidelines to just this: "Minimize the places where updates happen" "as long as the data model is correct => minimal amount of information needed" then you shouldn't be able to mess up the user interface.

I see what you're getting at, but I don't think it has anything to do with purity in this particular case. My point is that componentization (again, unless the components are actually independent) often gives unnecessary boilerplate and introduces chance of bugs in state synchronization. On the other hand, splitting M, V and U by themselves does not lead to unnecessary boilerplate or state duplication, and IMHO are in fact easier to understand than coupled components.

I think I am in the wrong here, I though that you dismissed components altogether which is why I assumed your purist position but no, I think we are pretty much looking for the same thing (yes I looked up your links and they were very helpful)

thanks a lot for sharing your insights, I really appreciate it!

@cmeeren
Copy link

cmeeren commented May 7, 2019

I agree with basically everything you just said. :)

Of potential relevance to the topic at hand; I had an epiphany last night while working with/on Elmish.WPF. I'll write a lengthy issue on the Elmish.WPF repo describing it (because it requires some breaking changes to Elmish.WPF), but I'd like to share my insights in this conversaion because I think it might be useful. The gist of it is this:

The problem with state duplication/synchronization in components is only really a problem if you have a separate view (or for Elmish.WPF, bindings) function for that component which does not know about the parent model (i.e., does not receive relevant parts of the parent model as parameters), because this means that any required parent state (whether duplicated or derived) must be stored in the child model so it can be used in the view. Cue all the previously mentioned state duplication/synchronization woes.

Thankfully, by following Richard Feldman's advice in this comment, you can split the model, message, update, and view separately and think of terms of "which information do I need" and end up with something that is almost its own separate MVU component, with a lot of the same benefits (understandable/testable in relative isolation) but avoids the drawbacks (state duplication/synchronization).

As an example, let's say you have a complicated form with a lot of inputs. Understandably, you might not want all those fields directly in your root state, or all the messages as cases directly in your top-level message type. What you can do is:

  • You split Model by writing a separate child model for the form inputs (and other form state)
  • You split Msg by writing a separate message type for the form messages, and a wrapper for those messages in the main message type (just as with a normal sub-component)
  • You split update by delegating all child messages to a separate update function for the child model and the child message type, and which also accepts any needed part of the global state
  • You split view like you normally split it, accepting the child model, and any needed parts of the parent model.

Splitting all of these by themselves and thinking in terms of which information they need avoids the problem of state duplication/synchronization, and lands you with something that is as easily testable as a separate component.

For example, let's say you have some autocomplete in the form, based on a global list of entities in your main model. This autocomplete is the responsibility of the view, which must then accept that list of entities (along with the child model). And as another example, let's say that a particular branch of the child update function also needs access to a global list of entities. This must then be passed to the child update function, so that any branch that needs it has the latest list of entities at the time of an update.

If we had designed the form as a completely separate sub-component (not accepting any "extra" data in any functions), the model would need a duplicate of the parent's entity list to be used in the update and view functions, and the parent update would have to remember to update the child model whenever the relevant parts of the parent model changed. It's very easy to miss.

Care must of course be taken to not store any derived state in the child model, too (the same goes for the parent model by itself, of course, but it's more insidious for child models). For example, a list of pre-processed entity names must not be stored for the autocomplete. If this was done, that would mean that if the parent model's list of entities is updated, the child model's entity name list becomes out of date (because the child model is only updated for child messages, so it doesn't know that the parent model's list of entities is updated). Any derived state must be the responsibility of the view function (which must then accept the relevant parent state, and thus always sees the up-to-date data).

(Note that in some cases, I've seen derived state as a proposed solution for avoiding repeating expensive computations, but AFAIK a sprinkling of memoization and lazy views solve this without the need for duplicated state.)

Let me know if this was unclear.

@aspnetde
Copy link

aspnetde commented May 13, 2019

I followed the advice to start with one simple App.fs while building my little app in the last couple of days. However, I don't like the idea of having one large file and I think in the context of Fabulous it makes sense to split things up at the level of actual pages.

This is what I came up with tonight. Actually I think it's similar to what @TimLariviere has built with the ElmishContacts sample.

I decided it would be a good idea to have the pages access some global state as well, but not the state of all their siblings. So I introduced some GlobalModel (I will probably rename it to SharedModel) that can hold things like session information etcetera.

The app's view and update functions are still the main integration points that will grow and become a bit ugly over time. But I realized that at least regarding the view function I already have such a central point in my MVVM applications as well – using view presenters. The huge amount of boilerplate code in the central update function is unfortunate, but I think I can live with that for now.

@cmeeren
Copy link

cmeeren commented May 14, 2019

I followed the advice to start with one simple App.fs while building my little app in the last couple of days. However, I don't like the idea of having one large file

Perhaps this tip is related to live update currently being limited to one file only? Personally I'd never want to place all code in a single file unless it's just a demo app (or another just as trivial app - I don't know of any real apps that are that simple.)

and I think in the context of Fabulous it makes sense to split things up at the level of actual pages.

I absolutely agree. This makes sense with almost any complex part of an Elm/Elmish app. :)

The key word is "split", though. In light of the recent discussion (and particularly my previous comment), I'm eager to learn: What do you get with the GlobalModel approach that you do not get if you simply parametrize the child update and view functions by all the state they need (and no more)? The separation is the same (including the possibility of a separate ChildMsg), but in the case of the GlobalModel approach, the difference is that it centrally defines the "extra" model which all sub-components must receive in update and view (instead of just passing whatever they need as params or a custom type), and the child update is responsible for updating GlobalModel in addition to their own model.

I can imagine GlobalModel growing quite big as the app grows, containing state that most views don't need themselves. I see parallels to the interface segregation principle where classes receive interfaces that let them do much more than they need to. Furthermore, since GlobalModel updated all over the place, that part is a bit more unclear, too (IMHO).

@aspnetde
Copy link

aspnetde commented May 14, 2019

Perhaps this tip is related to live update currently being limited to one file only? Personally I'd never want to place all code in a single file unless it's just a demo app (or another just as trivial app - I don't know of any real apps that are that simple.)

I guess It was my conclusion of this talk https://www.youtube.com/watch?v=XpDsk374LDE. Probably it doesn't even matter how complex an app actually is, as long as more than one person is working on it at the same time you will sooner or later run into merge conflicts. (iOS Storyboards and Android Layouts calling...) But I think we agree on that.

What do you get with the GlobalModel approach that you do not get if you simply parametrize the child update and view functions by all the state they need (and no more)?

Simplicity. Each page gets its own model, and besides that there is one shared global state for information that concerns the whole app, like session information (try to think of something like IsAuthenticated). In Redux every reducer action gets access to the whole state and not just a subset of it. This turned out to be very pleasant to work with.

Of course I could just pass the information needed for a particular page to its update and view functions, but that would require to have some "page specific" logic in App.fs. This way its all treated the same way, so wiring up a new page becomes a no-brainer.

Disclaimer: I obviously have no idea if this is going to work out. It's still just an early thought-experiment. And the app I am building is fairly small, so I won't be able to make some profound conclusions in the near future.

I can imagine GlobalModel growing quite big as the app grows, containing state that most views don't need themselves. I see parallels to the interface segregation principle where classes receive interfaces that let them do much more than they need to. Furthermore, since GlobalModel updated all over the place, that part is a bit more unclear, too (IMHO).

Maybe. I think it's a trade-off between having to implement page-specific stuff at a central integration point, and dealing with the global model all the time at page level. It certainly would require some discipline and care-taking regarding the structure of the global model. On the other hand I can say that one global shared model works pretty well in the Redux world.

--

Edit: I got the same feedback now 2x. I will probably try to go firsts with just passing along the information needed to the pages. Let's see how this works out. Thanks for the feedback @cmeeren!

@cmeeren
Copy link

cmeeren commented May 14, 2019

Of course I could just pass the information needed for a particular page to its update and view functions, but that would require to have some "page specific" logic in App.fs.

Only if passing parts of the app state as parameters is considered page-specific logic. :)

I didn't mean that you needed the parent update or view to perform child-specific transformations. You can just pass directly the parts of the parent model that the child needs. And often it's easy to pass too much rather than exactly what's needed, which is a perfectly valid thing to do in order to simplify. For example, if a page needs just some of the signed-in user info, pass it the whole AppModel.SignedInUser object even though it might not need all of it.

Furthermore, building on your authentication-related case: What if the user isn't signed in? Then GlobalModel.SignedInUser might be None, and very little in the child view function would make sense. If you instead make the child view function accept SignedInUser (non-option), then first of all it becomes simpler by itself, and secondly you can only call it when you actually have a SignedInUser (so that you can do proper view composition using Option.map/pattern matching, rendering something different if it's None).

This way its all treated the same way, so wiring up a new page becomes a no-brainer.

I get what you mean, but you might need to extend GlobalState when adding a new view. That would be the same as just passing whatever is needed as params to the child update/view. And you have globally forced the added complexity of updating two models instead of one.


Personally I think that passing the required params is clearer, simpler, and safer. But I appreciate that that different people think differently. Do let us know if you try both and find out that GlobalState is actually better in some way! :)

@cmeeren
Copy link

cmeeren commented May 14, 2019

I just came across this excellent article by @MangelMaxime. The "Everything is a function" section is basically saying the same thing I've been trying to here. :)

@aspnetde
Copy link

Have you seen my edit above? I agree :-).

@cmeeren
Copy link

cmeeren commented May 14, 2019

Yep, I just wanted to mention the great article. :)

@TimLariviere
Copy link
Member

TimLariviere commented Jul 6, 2019

Closing the issue since the initial question has received some answers.
If someone wants to continue the discussion, please open a new issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
s/needs infos The issue needs to be completed
Projects
None yet
Development

No branches or pull requests

7 participants