SPA ports on page init

I recently ran into a situation that was tripping me up, so I thought I’d share the solution here in case it’s not common sense.

First a little background:

I needed to call out to a port when initializing a page in order to load audio. The port takes a url from the page, loads it as audio, and then sends audio metadata back to the page. What was interesting was that the port worked fine until I added a command to another page on init.

The port worked in this example scenario:

Main.elm:

type Page
    = Home Home.Model
    | Inner Inner.Model
    | NotFound


type alias Model =
    { key : Nav.Key, page : Page }

...

subscriptions : Model -> Sub Msg
subscriptions model =
    case model.page of
        Inner model_ ->
            Sub.map InnerMsg <| Inner.subscriptions model_

        _ ->
            Sub.none

Page/Home.elm:

init : ( Model, Cmd Msg )
init =
    ( Model Success, Cmd.none )

Page/Inner.elm:

port innerPortCmd : E.Value -> Cmd msg


port innerPortSub : (E.Value -> msg) -> Sub msg

...

init : ( Model, Cmd Msg )
init =
    ( Model Loading, innerPortCmd <| E.int 0 )

...

subscriptions : Model -> Sub Msg
subscriptions model =
    innerPortSub <| GotInnerPort << D.decodeValue D.int

The port did not work when I added a command to a separate page on init:

Page/Home.elm:

init : ( Model, Cmd Msg )
init =
    -- ( Model Success, Cmd.none )
    ( Model Loading, Task.perform GotTime Time.now )

Page/Inner.elm:

subscriptions : Model -> Sub Msg
subscriptions model =
    -- this never got called
    innerPortSub <| GotInnerPort << D.decodeValue D.int

I should have realized that I couldn’t depend on model.page to be immediately up to date (in Main.elm subscriptions), but since I encountered the above behavior I had to scratch my head for awhile…

To fix the issue I used Process.sleep 0:

Page/Inner.elm

init : ( Model, Cmd Msg )
init =
    ( Model Loading, Task.perform Ready <| Process.sleep 0 )

...

type Msg
    = GotInnerPort (Result D.Error Int)
    | Ready ()


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GotInnerPort result ->
            case result of
                Err _ ->
                    ( model, Cmd.none )

                Ok num ->
                    ( Model (Success num), Cmd.none )

        Ready _ ->
            ( model, innerPortCmd <| E.int 0 )

This ensures that the model in Main.elm subscriptions has the correct page when the port is called!

Here is the full working example if anyone is interested: https://github.com/simplystuart/elm-ports-spa-example

I’m also curious to know if there is a better way to do this. Thanks!

This looks like a bug to me.

subscriptions is called by the runtime after update and so should get value for model returned from the last call to update. The extra call to update you’ve added by having the Ready msg has allowed subscriptions again with the value of model returned from the previous call to update

It looks like it’s already been reported.

Does it look similar to https://github.com/elm/core/issues/896 to you? Would it work just fine in your case if you change ports declaration order?

@gyzerok, switching the order in which my ports are declared did not solve the issue, so I’m assuming this is related to the issue @jessta mentions. Thank you both!

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.