Giphtblog

What happens in Shared stays in Shared

2025-09-22 |
Estimated reading time: 9 minutes
Update 2025-09-23

There is a thread from 2024-02-21 in the Elm Land Discord where some poeple are discussing this. The OP’s proposed solution is exactly the same as the one I discovered and described in this article. Unfortunately, they did not find a better solution by the time the thread died down, and the solutions they discussed were either incompatible with Elm Land’s CLI, or one of the proposed solutions that do not work that I’ve written about in this article.

Elm Land seems to be a very established framework for more complex SPAs in the Elm ecosystem, but I have personally found one very glaring omission from its architecture that I want to highlight today.

Prerequisites

This post assumes you’ve ran the following in your repo at some point:

1elm-land customize shared
2elm-land customize effect
3elm-land customize js

This exposes necessary files from the Elm Land architecture that we need to fiddle with.

The Problem

Pretend we’re making a Battleship-like game, and you have two screens, “Map” and “Targets” that share some state. “Targets” show you a list of potential targets you can strike, while “Map” shows all targets on a map. Both distinguish between targets you have destroyed and those you haven’t.

So it makes sense to keep the state of destroyed targets in Shared.

A short refresh on Elm Land’s architecture, this is how the pages and Shared would be structured in the application’s main Model: this:

1Main
2├ Shared
3└ Page
4  ├ Targets
5  └ Map

From each of the screens, you can pick a target to fire missiles at. So in your Effect.elm, you have defined a function like this:

1fireTheMissiles : Target -> Effect msg
2fireTheMissiles target =
3  Shared.Msg.FireTheMissiles target
4    |> sendSharedMsg

And in Shared.elm, your update function handles FireTheMissiles like this:

 1update : Route () -> Msg -> Model -> ( Model, Effect Msg )
 2update route msg model =
 3  case msg of
 4    -- ...
 5    FireTheMissiles target ->
 6      ( model
 7      , Api.fireTheMissiles target (MissilesFired target)
 8      )
 9
10    MissilesFired target result ->
11      case result of
12        Ok () ->
13          ( { model | destroyedTargets = target :: model.destroyedTargets }
14          , Effect.none
15          )
16        Err e ->
17          -- ...

This works well: both Map and Targets will stay in sync with each other as targets are destroyed.

But what if you want to show a message box of some sort on Map if you destroy a target, or if you want to have an animation shake the target in the Targets list as it is destroyed. You need the pages to react to the event of the target being destroyed, not just represent the current state of the list of destroyed targets.

You need to tell Map and Targets when the target is destroyed. You need to send a message to Map and Targets from Shared’s update function.

And this is where Elm Land just has a big gaping hole in its implementation. There is no supported mechanism, infrastructure or technique to neatly send a message to a page from Shared.

But there are workarounds.

The only workaround I found that works

We effectively have to have a mechanism for sending messages from Shared to a receiver Shared does not know about. The only way to do this, I found, is using a port, which gives us a Sub for the pages to subscribe to. By sending the messages from Shared to the JS side and immediately just returning them to the Elm side unchanged, we can effectively send messages from Shared to a Sub that other pages have subscribed to.

To distinguish “internal” and “external” messages of Shared.Msg, we’ll introduce Broadcasts. Named so because anyone can listen in from anywhere, with the implementation we’re going for.

src/Shared/Broadcast.elm:

1import Json.Decode as Decode exposing (Decoder)
2import Json.Encode as Encode
3
4type Broadcast
5  = MissilesFired (Result ())
6
7decoder : Decoder Broadcast
8
9encode : Broadcast -> Encode.Value

We need to be able to send a broadcast out from within Shared, which means we need a new Effect constructor.

src/Effect.elm:

1type Effect msg
2  = -- BASICS
3    -- ...
4  | SendSharedMsg Shared.Msg.Msg
5  | Broadcast Broadcast

This will go to a port:

src/Effect.elm:

 1toCmd options effect =
 2  case effect of
 3    -- ...
 4    Broadcast b ->
 5      broadcastOut <| Broadcast.encode b
 6
 7
 8port broadcastOut : Encode.Value -> Cmd msg
 9
10
11broadcast : Broadcast -> Effect msg
12broadcast =
13  Broadcast.encode >> broadcastOut >> sendCmd

Next, we need a way for pages to subscribe to broadcasts:

src/Effect.elm:

1port broadcastIn : (Encode.Value -> msg) -> Sub msg
2
3{-| Convenience function for listening to broadcasts
4-}
5onBroadcast : (Result Decode.Error Broadcast -> msg) -> Sub msg
6onBroadcast toMsg =
7  broadcastIn (Decode.decodeValue Broadcast.decoder >> toMsg)

And with that, our pages can subscribe to Broadcast messages sent by Shared (as well as anyone else in the application).

src/Page/Map.elm and src/Page/Targets.elm:

 1subscriptions : Model -> Sub Msg
 2subscriptions _ =
 3  Effect.onBroadcast Broadcast
 4
 5
 6update : Msg -> Model -> ( Model, Effect Msg )
 7update msg model =
 8  case msg of
 9    NoOp ->
10      ( model
11      , Effect.none
12      )
13
14    Broadcast _ ->
15      -- Do fancy stuff here
16      ( model, Effect.none )

And to fire the missiles:

src/Effect.elm:

1fireTheMissiles : Target -> Effect msg
2fireTheMissiles =
3    FireTheMissiles >> SendSharedMsg

src/Page/Targets.elm:

1update shared msg model =
2  case msg of
3    -- ...
4    Fire target ->
5      ( model
6      , Effect.fireTheMissiles target
7      )

Trouble in paradise

This solution is far, far from perfect, or even “decent” to many people. To list a few issues:

But I have yet to find a solution to this problem that actually works apart from this one.

Alternatives that do not work

In my search, these were some potential solutions I explored that turned out not to work.

Callback message

This would’ve been the best implementation of this, had it not been for Elm Land getting in the way.

Consider Http’s functions. They take a expect with constructs a message for the component to receive the response with. What if we too could take a message constructor to send off when the missiles have been fired?

src/Effect.elm:

1fireTheMissiles : Target -> (Result Http.Error () -> msg) -> Effect msg
2fireTheMissiles target callback =
3  FireTheMissiles target callback
4    |> SendSharedMsg

But that means Shared.Msg needs to be parametrized over msg:

src/Shared/Msg.elm:

1type Msg msg
2    = NoOp
3    | FireTheMissiles Target (Result Http.Error () -> msg)

Which means Shared.init, Shared.update, etc. needs to take into account this msg parameter, which means they now have to look something like this:

src/Shared.elm:

1init : Result Json.Decode.Error Flags -> Route () -> ( Model, Effect Msg )

Which Elm Land does not like:

 1-- UNEXPECTED TYPE ANNOTATION -------------------------------- src/Shared.elm
 2
 3I found an unexpected type annotation on this init function.
 4
 5  init : ... -> ( Model, Effect (Msg msg) )
 6         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 7I recommend this annotation:
 8
 9  init : ... -> ( Model, Effect Msg )
10
11Although Elm annotations are optional, Elm Land requires an annotation for
12this particular function to avoid showing errors in generated code.
13
14Hint: Read https://elm.land/problems#unexpected-type-annotation to learn more

Which is a real bummer…

Explicitly sending messages to individual receivers from Shared

Let’s invert things a bit: how about constructing Msg values for each receiver from within Shared? Since Shared.update can use Effect, we can just Effect.sendMsg to each page, right?

Apart from this sounding incredibly tedious for any cases where you have nested components that want to use broadcasts, it doesn’t even work.

Elm Land does not give you access to the core update function that passes messages to the pages, so you cannot construct whatever Msg it requires to send a page message, and thus you cannot construct the message, however long and tedious it would be, to target a specific receiver from within Shared.

Passing broadcasts upwards along the chain of messages like with Shared.Model

Same problem as above: without access to the underlying update function, we cannot adapt Elm Land to let us do this. Besides, there’s a separate problem with this potential solution:

For a given page, this is how its update function would end up looking:

1update : Shared.Model -> Broadcast -> Msg -> Model -> (Model, Effect Msg)

So if you have a Broadcast to send to it, but not a new Msg, how do you call this function? I guess you need a separate function for dealing with broadcasts to go along with your regular update:

1update          : Shared.Model ->       Msg -> Model -> (Model, Effect Msg)
2updateBroadcast : Shared.Model -> Broadcast -> Model -> (Model, Effect Msg)

This does not spark joy, and you cannot connect the plumbing correctly to begin with, so it’s a dead end.

Create a Sub in Elm

This would prevent us having to send stuff out to the JS side of things only to receive it in return immediately, but this simply is not possible to do in Elm.

There are no mechanisms to create a Sub in pure Elm code, only through ports.

All the module that create Subs are kernel modules that use JS in some way and thus uses Subs to interface with that JS code.

Conclusion

I have yet to see anyone talk about this problem at all, and that surprises me greatly. Granted, Elm is pretty niche to begin with, and the number of Elm Land users realistically might only number in the hundreds, so it shouldn’t surprise me that I don’t find anyone talking about this given how little talk there is about Elm Land in general. There are just not enough people using Elm Land for there to be enough talk about it to have someone ask these questions, I suspect.

So this is my contribution to that non-existent discussion: how do we do shared messages in Elm Land? What’s a better way to solve this? I’m genuinely curious: I hate that I have to resort to these hacky solutions in this case.

A full (contrived) example utilizing this solution is available at on my GitHub.