Send command when element rendered or am I stuck in old React thinking?

I’m new to Elm and rewriting an app from React/Redux. Thus far, it’s been immensely satisfying and I’ve been able to find answers on my own except for this problem. That is the situation where I want to take some action when I first conditionally render something and would normally in React use componentWillMount.

In my situation, I have a chat box that is rendered on two routes, but only if either the window size is at desktop width or it’s at mobile width and the chat box “tab” is selected. The behaviour I want is to fetch the chat messages when the chat box is first shown, not when the route is first loaded. I currently have the chat “component” view/update/messages/commands neatly in one separate file which works wonderfully except for initiating this fetch.

I know I could wait for window resize updates and route changes and then only when both conditions are satisfied try to pass a message down to the route update and then chat update function, but that seems like a lot of cognitive overhead that goes into remembering which routes will need the update, etc.

Instead, I’ve left an img tag on the chat view which loads a short gif string, and on load triggers the fetch command. This has worked perfectly but seems like a mega-hack and feels wrong.

chatView model =
  div [ class "chat-view" ]
    [ img [ src shortBase64Gif, on "load" (Decode.succeed FetchMessages) ] []
  ...

Is there a simpler solution I’ve missed based on some assumption about how this should be implemented? What might go wrong if I left the code as is?

Thanks for any help

1 Like

There is an issue related to what your are trying to do:
https://github.com/elm-lang/html/issues/19

But I think it can be achieved now with this:

2 Likes

Can you explain what you mean with “which routes have to get the update”?
Does it mean you include the chat multiple times in different pages?

Exactly

I have a chat box that is rendered on two routes

So each route has a view with it’s own messages and update function. But since the chat depends on the window size, and the route updates won’t be invoked unless I explicitly tell the case for Window.resizes to also check the route(?) and run the update function for it to get the fetch command, the user could resize the window to show the chat and the fetch command would never trigger.

I apologize if I’m mixing any of the terminology inappropriately. Much of it still new to me.

You could also store the window size in the model and use that in the on-route-change branch of your update. That seems to keep the check closer to where it’s relevant.

1 Like

What @szabba said is what I do. When my programs need the window size, I just pass in the window size in the flags and then subscribe to window resizes. If you know you have the window size at initialization, and you know you will get updates whenever the window changes, you know you will always have accurate information regarding the window’s size. Our approach is to have the information long before you need it.

Let me know if that doesnt really solve your problem. I feel like this answer doesnt address what you actually asked, but I suspect you what you actually asked wasnt exactly what you needed to begin with because you didnt know there would be a way for rendering and routing to happen simultaneously.

It doesn’t really solve my problem, but again thank you all for your assistance. I’ll give it another shot, since I clearly didn’t lay out my problem well before.

You could also store the window size in the model

I do. My main update function stores the window size exactly as @Chadtech said and then on my route change branch I’ll call SomeRoute.update with a message like SomeRoute.Init and if it implements the chat component it will call Chat.update which returns the fetch command and that works great if the chat is supposed to be displayed as soon as I switch to the route.

But in the case where it’s not being displayed yet – say the window is too small – I don’t want to fetch yet. And if the user then resizes the window it will update the model and render the view again with the chat box visible but only Chat.view was called - nothing called Chat.update to get the fetch command.

Since Chat.view already gets the latest model I was just abusing the onload event to emit the fetch command but I was hoping for a more elegant solution. For example, I could make the window resize branch call the update functions of only those routes which implement chat, but then I have to remember to update that list. Or I could have it call the update function of the current route and have them all handle a WindowResize message, which is easier to remember, but that adds work for every new route.

Just curious how one of you more experienced folks would handle this. Hope this was clear enough. Thanks again!

If I understand the problem, then I would just put some conditional logic under the WindowSizeUpdated message that checks for the right route and if the window is now big enough, and if those conditions are met, then it mixes in the fetch command, otherwise is mixes in Cmd.none.

    WindowSizeUpdated newSize ->
        model
            |> setWindowSize newSize
            |> checkIfBigEnoughForChatBox

setWindowSize : Size -> Model -> Model

checkIfBigEnoughForChatBox : Model -> (Model, Cmd Msg)
checkIfBigEnoughForChatBox model =
    case (model.page, windowIsBigEnoughForChatBox model.windowSize) of
        (Page.Home _, True) ->
            (model, fetchMessages)

        _ ->
            (model, Cmd.none)

Do you feel that something is wrong with that approach? To me thats just how to do it, and it seems easy enough, but maybe you are registering this as unideal. Or maybe I just still dont understand.

1 Like

I suppose that makes sense. It seemed quite simple to let the Chat module handle all the fetching, that way only the direct “parent” of the chat had to call its view and update. With this strategy I’ll have to fetch messages in my Main module after window resize and in at least one other place in the case that it’s triggered by switching tabs or routes. It’s just more code in more places.

Guess there’s no magic I was missing. Appreciate the help!

Yeah you are welcome @sanlon, but one last thing- just because you mention you want the Chat module to do the fetching. I think you probably could have the Chat module do the fetching in a clean way. Heres kind of a sketch of how to do it:

0 Make a “global stuff” object on your main model, that includes things like windowSize that arent specific to any particular page or sub-module, and are just generally useful.
1 Pass your “global stuff” thing into your page level update functions, so they should look like this update : GlobalStuff -> Msg -> Model -> (Model, Cmd Msg)
2 Make a ChatBox.init function that returns (ChatModel, Cmd ChatMsg), where your chat message is your fetching cmd.
3 Your page level model, having access to the global stuff can see what the window size is, and it can check if its big enough for a chat box, and if it is it can invoke ChatBox.init on its own.

1 Like

The problem was never really having access to the global stuff on the page update, but rather getting the page update called when the window was resized. So my update looks something like

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    OnLocationChange location ->
        case (parseLocation location) of
            HomeRoute ->
              Home.update Home.Init model
        ...
    WindowResize size ->
      { model | window = size } ! []
    ...

Here Home.update has access to model.window but when a WindowResize happens, it updates the model but nothing calls Home.update again. No fetch occurs even if the chat window is brought into view by the resize. So I’m planning to modify the WindowResize branch to be more like

  ...
  WindowResize size ->
    let
      resizedModel = { model | window = size }
    in
      case model.route of
        HomeRoute ->
          Home.update Home.WindowResize resizedModel
        _ ->
          resizedModel
  ...

and let Home.update decide to tell Chat.update whether it’s being shown along with the rest of its state

case msg of
  WindowResize -> -- This is Home.WindowResize
    let
      (chatModel, chatCmd) = 
        Chat.update (bigEnoughForChat model) Chat.Refresh model.chatModel
    in
      ( { model | chatModel = chatModel }
      , chatCmd |> Cmd.map ChatMsg
      )
  ...

where Chat.update can decide when to return the fetch command.

1 Like

You could factor out a helper for “add a fetch command if the chat should become visible” and a “basic update” function and have the “real update” call both. The helper type would be smth like Model -> ( Model, Cmd Msg ) -> ( Model, Cmd Msg ). I can try to sketch that out after work.

Another way would be to call that helper in both branches.

1 Like

So here’s an Ellie. Some simplifications:

  • I use a button switching between two states instead of routing.
  • fetchIfOpenAndBigEnough might need a better name.
  • I fetch the current time, not some data over HTTP.
  • The view just has he button and a text dump of the model.

If your view needs to conditionally show the chat then you could re-use the same predicate function in your view and your version of fetchIfOpenAndBigEnough.

Hope this helps!

2 Likes

Thanks for going through the trouble to put that together! I haven’t tried piping all of update through a helper like that before and I could see using that in different ways in the future.

I initially didn’t like “coupling” the view logic with the fetching logic in two different places but like you said if I re-use the predicate function it doesn’t sound bad.

Thanks for the suggestions!