Re: Inter-Component Communication in 0.17

1,025 views
Skip to first unread message
Message has been deleted

Wayne Choi

unread,
Jul 15, 2016, 3:21:12 AM7/15/16
to Elm Discuss
Thanks for posting this!

On Tuesday, July 12, 2016 at 9:28:18 PM UTC-7, Erik Lott wrote:
As a quick note to anyone struggling with how to perform app wide inter-component communication in elm 0.17 (e.g. anyone building a non-trivial SPA), here is one simple way to setup pubsub style communication using Elm Ports and Subscriptions. 

In this example, ChildOne will broadcast a message that ChildTwo is listening for.

Ports.elm
port module Ports exposing (..)

-- dispatchSomeMessage will create a command with a
-- string payload representing an important message
-- to broadcast to listening components.

port dispatchSomeMessage : String -> Cmd msg


-- receiveSomeMessage is the port which our components
-- subscribe to receive the dispatched message

port receiveSomeMessage : (String -> msg) -> Sub msg


The Ports.elm file contains all of the port specifications in the app. In this case it defines a two ports: one port for dispatching a message, and another for receiving a message. The module declaration at the top of the file must be preceded by "port".

index.js
var app = Elm.Main.fullscreen();

app.ports.dispatchSomeMessage.subscribe(function(msg) {
    app.ports.receiveSomeMessage.send(msg);
});

This is where the behaviour of all ports are defined in javascript. In this case the dispatchSomeMessage function is receiving a msg argument from Elm, and then quickly sending that message back into Elm through the receiveSomeMessage port.

ChildOne.elm
module ChildOne exposing (..)

import Ports exposing (..)
import Html exposing (Html, button, text)
import Html.Events exposing (onClick)


type alias Model = String


type Msg
    = Click

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Click ->
            ( model, dispatchSomeMessage "Hello World!" )


view : Model -> Html Msg
view model =
    button [ onClick Click ] [text "Click Me"]


ChildOne dispatches the message "Hello World!" through the dispatchSomeMessage port when a button in the view is clicked. The message is then routed from the dispatchSomeMessage port, directly back into the receiveSomeMessage port as defined in index.js

ChildTwo.elm
module ChildTwo exposing (..)

import Ports exposing (..)
import Html exposing (Html, text)

type alias Model = 
        { message : String }

type Msg
    = ChangeMessage String

subscriptions : Model -> Sub Msg
subscriptions model =
    
receiveSomeMessage ChangeMessage


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ChangeMessage msg ->
            ({ model | message = msg }, Cmd.none )


view : Model -> Html Msg
view model =
    text model.message

ChildTwo subscribes a ChangeMessage tag to the 'receiveSomeMessage' port. When the port receives the "Hello World" message from ChildOne, ChildTwo updates its models message, and re-renders the view.

Main.elm
module Main exposing (..)

import ChildOne
import ChildTwo

type alias Model =
    { childOneModel : ChildOne.model
    , childTwoModel : ChildTwo.model
    }

-- ..init func
. nothing special here. 

type Msg
    = ChildOneMsg ChildOne.Msg
    | ChildTwoMsg ChildTwo.Msg


subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ Sub.map ChildOneMsg (ChildOne.subscriptions model. childOneModel)
        , Sub.map ChildTwoMsg (ChildTwo.subscriptions model. childTwoModel)
        ]

-- .. update function. nothing special here. 

main : Program Never
main =
    Html.App.program
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }



Main.elm batches the subscriptions defined in child components. That's all there is to it. Nice and clean inter-component communication.

Andrés Otárola

unread,
Jul 15, 2016, 2:14:24 PM7/15/16
to elm-d...@googlegroups.com
Erik, I'm using this approach, it is very clean !

Cheers~~

--
Andrés Otárola Alvarado
Software Engineer
Skype: andres.otarola.alvarado
Cel Chile: (+56) 8 415 2618

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Max Goldstein

unread,
Jul 15, 2016, 8:08:23 PM7/15/16
to Elm Discuss
Allow me to suggest that this approach is *totally overkill*. It might not be, but this is something Richard and I were discussing last night at the meetup. If you come from React, everything is a component, so you reach for them. But, for many uses cases, nesting TEA is unnecessary. Go ahead and make Model and Msg huge types. The compiler will keep you from breaking things.

Erik Lott

unread,
Jul 16, 2016, 12:49:41 AM7/16/16
to Elm Discuss
Max, build a non-trival 15+ page SPA within a single elm component, and tell me how that goes :)

art yerkes

unread,
Jul 16, 2016, 2:53:05 AM7/16/16
to Elm Discuss
Try testing any part of such a big ball of mud.  TEA components can even be poked in the repl, which you can't do with a Main module easily.

Max Goldstein

unread,
Jul 16, 2016, 5:33:52 AM7/16/16
to Elm Discuss
...well, I suppose at that size it makes sense :)

Just don't go componentizing every little text field or icon.

Wayne Choi

unread,
Jul 19, 2016, 4:32:47 AM7/19/16
to Elm Discuss
Max, 

Is there a rule of thumb for when to create new modules rather than keeping a project in one module/component? 

Max Goldstein

unread,
Jul 19, 2016, 10:23:35 AM7/19/16
to Elm Discuss
Split off components when you can't stand the monolith for a moment longer. Or, I suppose, if you need to reuse something stateful (but consider a view function with the state as an argument first).

Aislan de Sousa Maia

unread,
Jul 21, 2016, 12:41:09 PM7/21/16
to Elm Discuss
Hello guys! I'm a kind of noob with all of this. Can you explain a little more the concern regarding componentization? Some examples could be great.

Thanks for all!

Erik Lott

unread,
Jul 21, 2016, 2:01:26 PM7/21/16
to Elm Discuss
Hey Aislan,

Max's point of avoiding unnecessary components is a good one, but it depends on what scale of app/architecture you're talking about.

When you're building building smaller types of UI components, it's a good rule to get you started. For example, let's say you want to build something simple like an html form in elm. You may want some input fields, select boxes, error messages, and maybe a custom submit button (whatever that is). This can likely be built within a single TEA component, without the need to create any additional subcomponents. Creating subcomponents for individual inputs, or special buttons, etc will likely be an unnecessary abstraction that will only make your code harder to understand, and harder to maintain. On the small scale, just avoid unnecessary componentization and you'll be fine.

When you're building something at a larger scale like a single page app, that contains various layouts, menus, &  pages - which contain several components per page - you'll need components to keep your code organized, maintainable, and easy to understand. 

The inter-component communication method that I posted at the top of this thread is likely only useful in a large app, where there's a need for app wide communication. Using this method for small scale parent-child communication probably overkill.

OvermindDL1

unread,
Jul 21, 2016, 2:30:30 PM7/21/16
to Elm Discuss
I actually took your format and made it more generic:
```elm
var app = Elm.Main.fullscreen();

app.ports.dispatchGlobal.subscribe(function({portName, msg}) {
    app.ports[portName].send(msg);
});
```
Where the dispatchGlobal would be:
```elm
port dispatchGlobal : {portName : String, msg : Json.Encode.Value} -> Cmd msg
```
And the receiving could be about whatever you want:
```elm
port receiveSomeMessage : ({something : Int, more : String} -> msg) -> Sub msg
```
Dispatching via a helper function like:
```elm
dispatchSomeMessage =
  dispatchGlobal {portName="receiveSomeMessage", (object [("something", int 42), ("more", string "blah")])}
```

Although I am using this style for a certain javascript callback library that Elm does not support yet and not needed it to toss messages 'far' away yet, and hope to not need to...

Erik Lott

unread,
Jul 21, 2016, 3:47:35 PM7/21/16
to Elm Discuss
It's a neat idea, but it makes me uncomfortable, because it introduces a possible source of errors into the application. What if a message is dispatched to the wrong port?

The code that I posted at the top of this thread is a simplistic example of how to dispatch messages across your app, but you probably don't want to be communicating with simple primitives (String, Int, etc). Just like you use union types for messages within TEA components, use union types for your app wide messages as well. 

For example, if you need to broadcast/subscribe to some app wide events, you could create an Events.elm module that isolates all of this logic:

port module Events exposing (Event(..), publish, subscribe)


type Event
    = SomethingHappened
    | SomethingElseHappened Int


type alias DTO = 
  (String, String)

-- Ports

port eventOutgoingPort : DTO -> Cmd msg

port eventIncomingPort : (DTO -> msg) -> Sub msg


-- PubSub

publish : Event -> Cmd msg
publish evt =
    outgoingEventPort (serialize evt)


subscribe : (Event -> msg) -> Sub msg
subscribe f =
    Sub.map f (incomingEventPort deserialize)

-- Serialization

serialize : Event -> DTO
serialize evt =
    case evt of
        SomethingHappened ->
          ("SomethingHappened", "")

        SomethingElseHappened int ->
            ("SomethingElseHappened", ... serialize int to json)


deserialize : DTO -> Event
deserialize ( key, json ) =
    case key of
        "SomethingHappened" ->
            SomethingHappened

        "SomethingElseHappened" ->
            SomethingElseHappened ... deserialize json to int


Dispatching an event looks like this:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        BlahMsg ->
            ( initialModel, Events.publish Events.SomethingHappened )

And subscribing to events looks something like this:

type Msg
    = Event Events.Event



subscriptions : Model -> Sub Msg
subscriptions model =
    Events.subscribe Event


I hope that makes sense.

Duane Johnson

unread,
Jul 21, 2016, 6:59:46 PM7/21/16
to elm-d...@googlegroups.com

On Thu, Jul 21, 2016 at 1:47 PM, Erik Lott <mreri...@gmail.com> wrote:
type alias DTO = 
  (String, String)

What does DTO stand for in your example?

Erik Lott

unread,
Jul 21, 2016, 7:09:52 PM7/21/16
to Elm Discuss
Data transfer object. Call it whatever you'd like :) It's the object that travels in and out through the port.

Junhui Park

unread,
Oct 22, 2016, 10:13:39 AM10/22/16
to Elm Discuss
Thank you so much. Your post saved my day. Cheers.
subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Click ->
            ( model, dispatchSomeMessage "Hello World!" )


view : Model -> Html Msg
view model =
    button [ onClick Click ] [text "Click Me"]

ChildOne dispatches the message "Hello World!" through the dispatchSomeMessage port when a button in the view is clicked. The message is then routed from the dispatchSomeMessage port, directly back into the receiveSomeMessage port as defined in index.js

ChildTwo.elm
module ChildTwo exposing (..)

import Ports exposing (..)
import Html exposing (Html, text)

type alias Model = 
        { message : String }

type Msg
    = ChangeMessage String
subscriptions : Model -> Sub Msg
subscriptions model =
    
receiveSomeMessage ChangeMessage


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ChangeMessage msg ->
            ({ model | message = msg }, Cmd.none )


view : Model -> Html Msg
view model =
    text model.message

ChildTwo subscribes a ChangeMessage tag to the 'receiveSomeMessage' port. When the port receives the "Hello World" message from ChildOne, ChildTwo updates its models message, and re-renders the view.

Main.elm
module Main exposing (..)

import ChildOne
import ChildTwo

type alias Model =
    { childOneModel : ChildOne.model
    , childTwoModel : ChildTwo.model
    }

-- ..init func
. nothing special here. 

type Msg
    = ChildOneMsg ChildOne.Msg
    | ChildTwoMsg ChildTwo.Msg


subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ Sub.map ChildOneMsg (ChildOne.subscriptions model. childOneModel)
        , Sub.map ChildTwoMsg (ChildTwo.subscriptions model. childTwoModel)
        ]

-- .. update function. nothing special here. 

main : Program Never
main =
    Html.App.program
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }

Richard Feldman

unread,
Oct 22, 2016, 10:50:38 AM10/22/16
to Elm Discuss
I agree with Max's original statement. I would give each logical "page" its own Model, View, and Update, and that's probably it.

This is effectively what we've done at NoRedInk with our 50,000 lines of production Elm code, and it has been extremely rare to find examples where nesting an entire model, view, and update further than that was a good idea.
When you're building something at a larger scale like a single page app, that contains various layouts, menus, &  pages - which contain several components per page - you'll need components to keep your code organized, maintainable, and easy to understand.  
I strongly disagree with this.

Every time I've followed this approach I've regretted it. It introduced substantial communication overhead, making my code harder to work with, and got me vanishingly little in return. When I've chosen not to do this, and to instead focus on localized refactors like "this record is too big, so I'll split it into a few smaller records" or "this Msg is too big, so I'll split into a few smaller union types," the code has ended up much easier to maintain at scale.

My strong impression is that the pattern presented in OP is a band-aid over a self-inflicted wound. I would advise against using it, and instead choosing not to self-inflict that communication overhead in the first place. Scaling by thinking in terms of "components" is a natural fit for React, and an extremely poor fit for Elm.

Don't do this to yourself!

Erik Lott

unread,
Oct 22, 2016, 11:28:35 AM10/22/16
to Elm Discuss
As the OP, I agree. Don't do this. We quickly abandoned this functionality after we wrote it. One of those posts I would I could remove...

Erik Lott

unread,
Oct 22, 2016, 11:30:00 AM10/22/16
to Elm Discuss
Problem solved! Post deleted (thumbs up)


On Saturday, October 22, 2016 at 10:50:38 AM UTC-4, Richard Feldman wrote:

Richard Feldman

unread,
Oct 22, 2016, 1:21:44 PM10/22/16
to Elm Discuss
Oh man, now I feel like a total jerk for my harsh tone.

My apologies, and thank you for the update Erik! <3

Erik Lott

unread,
Oct 22, 2016, 1:26:38 PM10/22/16
to Elm Discuss
You're such a jerk Richard :)

António Ramos

unread,
Oct 23, 2016, 12:58:23 PM10/23/16
to elm-d...@googlegroups.com
Were does this all stand?
can anyone post an example that all agree to understand the best way to
Inter-Component Communication in 0.17 ?!

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

Erik Lott

unread,
Oct 23, 2016, 2:34:15 PM10/23/16
to Elm Discuss
If you need to communicate between 2 or more enclosed pieces of functionality/modules, you'll need to orchestrate that communication via their parent.

Like I said, I regret posting the original solution that I did, since it was essentially circumventing the elm language. You should try to stay within elm as much as possible, unless you're truly missing a piece of functionally, such as working with files, etc.

Reply all
Reply to author
Forward
0 new messages