What happens in Shared stays in Shared
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:
- Everything we want to
Broadcasthas to have a JSON decoder and encoder. - Have to send things out to JS only to receive it back in Elm, incurring an unnecessary overhead.
- Other pages and components will see the broadcasts triggered by each other’s actions. It could be unclear what action caused a given broadcast to trigger.
- Have to remember to subscribe to
Broadcast. - Any page and component can send a
Broadcast, given how things are structured out of necessity. - This is an event bus, and it feels distinctly non-Elm-like.
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.