Modifying "parent state" from child page in an elm-spa-example-like architecture

Like any good (or so I assume) Elmer I’ve followed the architecture of elm-spa-example with separate page modules that are Cmd/Html.maped in Main. I’ll assume most of you are familiar with this.

Let’s say I have a ‘LoginPage’ module with its own Model, Msg, update and view, and then Main looking something like this:

type Page
    = LoginPage
    | ...

type alias Model =
    { user : Maybe User
    , page : Page
    }

type Msg =
    = LoginMsg LoginPage.Msg
    | ...

update msg model =
    ...

view model =
    ...

The question essentially is: How do I change Main.Model.user from LoginPage?

It so happens that elm-spa-example also basically does this, but it does so by storing the user in localStorage and using flags and ports and such to get at it from the root, which I think is kinda cheaty.

An approach that does work, and that I currently use, is to expose LoginPage.Msg(..) and match on that in Main.update, but that’s a bit leaky and messy since I have to exhaustively handle it in both update functions.

I could pass the LoginMsg constructor to LoginPage.view and wrap all the messages manually instead of using Cmd/Html.map, which would allow me to emit Main.Msg from LoginPage.view directly, but that would cause a dependency cycle between Main and LoginPage and resolving that just causes an even bigger mess.

So what do? Is there a cheat code that doesn’t involve FFI? IDCLIP?

4 Likes

I havn’t needed such messages yet so I don’t have personal experience about this, but I found this to be interesting: The Translator Pattern: a model for Child-to-Parent Communication in Elm

2 Likes

A pattern we use in this situation is to pass message constructors to the child’s update function, like this:

LoginPage.update { onMsg = LoginMsg, onUserMsg = UserMsg } msg model

And then that update function returns the parent`s message type, without having to know it (since it just uses the supplied function). The type would be

update : { onMsg : Msg -> parentMsg, onUserMsg : User -> parentMsg } -> Msg -> Model -> ( Model, Cmd parentMsg )

Not sure what this pattern is called but it’s very nice.

9 Likes

user behaves like a global value. You have 2 options to change it: either return it updated from the page’s update OR return a global Msg that does the updating at the top level.

I tend to prefer the second approach. The pages’ update function looks like this:

update : Session -> Msg -> Model -> (Model, Cmd Msg, Session.Msg) where session is the name I use for the global context.

In the main update the message handlers look like this:

    ContactsMsg cmsg ->
        let
            ( contacts, cmd, smsg ) =
                Contacts.update session cmsg model.contacts
        in
        updateSession smsg { model | contacts = contacts } (Cmd.map ContactsMsg cmd)

where updateSession has this signature:
updateSession : Session.Msg -> Model -> Cmd Msg -> ( Model, Cmd Msg )

Please note that this uses a pattern where all the models for all the pages are held in a record together with the global state but the pattern can be adapted to be used with the the way elm-spa-example keeps the page model.

7 Likes

Here is a complete example of how the users session can be held in a parent module, but manipulated from any child module (this is for 0.18, I am currently updating it for 0.19 - bigger job due to elm-mdl being deprecated).

When a child module wants to change the user’s session state, it uses one of these functions in the Auth module to build a message for the auth module:

login : Credentials -> Cmd Auth.Msg
refresh : Cmd Auth.Msg
logout : Cmd Auth.Msg
unauthed : Cmd Auth.Msg

The child module can return the Auth.Msg as a so-called ‘out message’. The parent then feeds that to the Auth module which provides an updated session state as its out message. The new session state is then updated in the top-level Model.

Yes, it is quite a lot of boilerplate to do such a simple thing. However, it does give me a re-usable Auth package, and enforces a clean separation of concerns between the purpose of the auth module, the parent module and each child module.

Before switching to out messages, I was using ports to do child -> parent comms behind the scenes. Also did this with native code, as the elmq package, which was perhaps not such a terrible idea, but could not be shared due to native code restrictions in packages. I therefore re-wrote my auth module using out messages, and I am actually happiest with that approach out of the 3 options.

local-storage may be being used to hold the session state, so that session state flows across browser tabs. A more secure way to do that is to use secure session cookies instead. That is only possible though if the API you are accessing is not cross-origin, which explains why local-storage and ‘Authorization’ headers is probably a more common way to do it.

2 Likes

Thanks everyone for awesome ideas!

I went for out messages, which seems like the cleanest and least intrusive approach (and, in retrospect, probably the most obvious), since it’s an additional messaging mechanism that doesn’t need messing the existing one. It can easily be generalized across all Page modules, and, if needed, built out with or reuse something more complex and contained like the ´Auth´ module.

I still have one question regarding this approach though. What do you do if the child page returns a Cmd along with the out message, and you want to run a Cmd of your own. Just Cmd.batch them together?

Passing message constructors I think is best suited when the child should be independent of the larger app structure and be easy to reuse anywhere, as it’s very intrusive in the child but very easy on the parent.

And the translator pattern seems like out messages embedded in the existing message structure. An interesting idea that might have some benefits, but overly complex for the simple messaging needs I have right now at least.

1 Like

The article I linked about translator pattern starts by shortly discussing out-messages and then concludes that:

This is really nice, but still leaves something to be desired. It requires the child to handle messages it might not need to see, just so it can pass them on to the parent".

Example with out-messages:

  1. child view generates html button [ onClick (Child.NewUser) ] [ ... ]
  2. parent view maps Child.NewUser to ChildMsg Child.NewUser
  3. parent update passes ChildMsg Child.NewUser to child update
  4. child update returns OutMsg.NewUser
  5. parent update handles OutMsg.NewUser

Same with translator pattern:

  1. child view generates html button [ onClick (Child.NewUser) ] [ ... ]
  2. parent view translates Child.NewUser to Parent.NewUser
  3. parent update handles Parent.NewUser

In this case child update doesn’t need to handle Child.NewUser at all and so it will be simpler.

Of course as you say, translator pattern is a bit complex to set up, so it depends on use case which one is better choice overall.

1 Like

The article I linked about translator pattern starts by shortly discussing out-messages…

Ah, yes I saw that but I didn’t really understand the complaint. I see what’s meant by your example though, but I don’t actually consider that a downside. I think it’s better to have view only concern itself with internal messages and have update deal with “outside stuff”. Partly that’s because it’s simpler (conceptually), cleaner and more contained, but it also seems more flexible (in this regard, it might not be in others). In my case I actually DO want the message to be handled in the child first, because I want it to emit a route command before sending a message to the parent. It’s not immediately clear how I’d accomplish that with the translator pattern, but I haven’t looked too deeply into it since it seemed much simpler to just use out messages.

I think the OutMsg approach works pretty well, but what Ive been trying out lately is the way Richard does it in the spa-example, which is somewhat demonstrated on this line of code (the user is inside session) https://github.com/rtfeldman/elm-spa-example/blob/master/src/Main.elm#L125

Basically, dont put the user in the main model to begin with. Have it so that each page stores the User in its own way. Then when you move from page to page, pull the User out of one page, and stick it in the next page.

3 Likes

Interesting approach, but I don’t really see the benefit. Perhaps if it’s a shared piece of data that’s manipulated across many pages, like if you have a login modal on each page, but otherwise it mostly just seems like a lot of plumbing. Could you elaborate on why you chose it?

I’m surprised there’s so many different approaches to this, that they’re all cool and interesting in their own way, and not one of them involves dirty hacks like I feared they would.

I think the concept is similar to the encoders, decoders. So you have a Session record, then you encode the Session in your first page Login record, then you get an updated Session from that page and you start again the process with the next one.

An issue that I find with OutMsg, is that more than one child level or nesting makes the intermediate modules handle the messages between their parent and child, and with the time I also end with message wrappers that are too long for the debugger tool like UserMsg ProfileMsg FormMsg ImputMsg "some input text".

Could you elaborate on why you chose it?

I guess the end-of-the-day reason is because applications need stuff like User functionality. But you already knew that and all these approaches serve that purpose. So its a matter of efficiency, which I think about as “what do I have to type, maintain, and think about in these different approaches”.

The OutMsg approach has…
0 An OutMsg type
1 An OutMsg in every case of the update, which is often nothing
2 A function for handling the OutMsg in the parent scope, for every page.

So at the very least with the session approach, you dont have any of that. To me, thats plumbing, and its gone.

But do you have new plumbing in the session approach, what is it?
0 Every page Model contains the field session : Session
1 Every page init function probably needs the signature Session -> Model
2 You need one function toSession : Model -> Session for your main model.

I think if you just add up the lines of code for each of these approaches, the session approach is much smaller. But, LOC isnt everything. Conceptually we lose the messy and complicated process of parent-child communication with the session approach. Losing a big messy concept from our mental space is great. I dont see the session approach adopting any new complicated things as a trade off.

3 Likes

You don’t need to handle `OutMsg´ for every page, just those that use it. Which in my case was just one.

0 and 1 were already in place, so that actually wouldn’t be extra plumbing in my case. I was thinking you’d also need a toSession function for each page’s Model, like elm-spa-example does, but I suppose you don’t if by convention you expose it as an alias to a record with a session field. I think the case for this is looking better.

I now need to initiate the rendering of modals/popup menus at root level from each page and am considering which approach to use. I already have OutMsg in place of course, but I don’t want modals and popups to persist between pages, so it would be better if each page owned this data instead of the main model. Looks like it might be time to convert then!

I’ve recently converted an app to the “session based” 0.19 elm-spa approach and it is just so much nicer to work with that the 0.18 version. I do have modals in my app and I’ve approached these by adding them to the page view functions so they have a type signature of:

view : Model -> { title : String, content : Html Msg, modal : Maybe (Html Msg) }

This seems to work pretty well for me so far.

1 Like

We use the outer message approach. Is simple to understand and works really well.

But we return a list, not one. When there is nothing to do we return [].

When calling nested updates we end up with a bunch of messages to process e.g. update the user, show a notification. Every update along the way can add to this list, very similar to Cmd.batch.

1 Like

Yes, this is what I’m doing now as well. Passing Html in the model doesn’t work well if you need it to actually change with the model as well. The problem (or rather annoyance) I have with this approach is that you either have to thread the modal through quite a few view functions and/or move the code far from where it belongs. And in my case the “modals” are actually dropdown menus that have trigger buttons which change depending on what you do with the dropdown, exacerbating the problem.

Putting the shared state in the pages and just handing it off when creating a new page works nicely to avoid the parent data problem but introduces the potential issue that there is nothing in the data structure to express that we only have one user at a time except to the extent that we only allow one page at a time. What if things evolved in a way where we didn’t want to show only one page — e.g., what used to be a separate page turns into a panel that opens on the side? If correctness depends on there only being one logged in user, then the make-illegal-states-impossible philosophy says you should store the user somewhere where it is forced to be unique. On the other hand, if things would be functionally correct but maybe a bit weird with user values that were out of sync, then pushing it down into the pages — particularly as a non-editable value — is a good way to reduce plumbing.

We use out messages in a way where they subsume commands. Update functions return a list of out messages and one of the standard cases for an out message is “send this command”. The code that would just map commands to add addressing instead maps over the list of out messages coming from the child applying changes to the parent and/or accumulating parent out messages for the next level. It’s more wordy than the normal Elm command architecture but it fits essentially the same pattern which is nice. The general notion is that out messages and commands are the same thing from the child’s perspective: a request for something to happen that is beyond the child’s ability to perform itself.

We used to use out messages for state queries as well — thinking of them as being much like HTTP queries — but this resulted in more convoluted control flow than was ideal with requests going from child to parent and then back to the child. So, we now handle ancestral state either by passing it down to functions like view and subscriptions which have no power to modify state anyway and broadcasting it to descendants when it changes for those cases where children want to respond in their own state to changes in ancestral state. I’m less thrilled about the latter and am looking at ways to eliminate it but the data structures and algorithms often get more complicated in the absence of the ability to react to parental state changes. But that’s its own topic.

Mark

5 Likes

I have been thinking about this pattern and I realized that I needed to convert parentMsg to Cmd parentMsg by a function like this,

send : msg -> Cmd msg
send msg =
  Task.succeed msg
  |> Task.perform identity

which was taken from the medium article. But in there, the author did not recommended it though.

Just putting an user information to a parent model on success login, so no noticeable performance penalty for my case. Maybe there might be a race condition issue since I am changing url at the same time. I need to test more at this point, otherwise looks like this solution is pretty clean for me.

Could you let us know if you are doing this something different way ?

1 Like

Yes, this is one drawback of this solution. If you see it as a drawback.

Another way to look at it is to see it as a complete separation of the components. Since the back communication is only through messages, you have tracability of that communication, which would otherwise get lost in the update function.

Perhaps you can explain what you mean by this being a race condition. Which command is the race with?

1 Like

I was talking about the race condition just referring the mentioned article. By inspecting my application again, I realized I could change url to a new page after receiving parentMsg in the parent update function, so no such problem happen.
Thank you for clarifying.