Skip to content

Commit d2778fb

Browse files
committed
publish elm-land shared post
1 parent 50e1879 commit d2778fb

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
---
2+
title: "Elm Land, Shared Subscriptions, and the Art of Workarounds"
3+
date: 2025-10-14
4+
description: "When you need to react to Shared model changes in Elm Land - is it a real problem, an architectural flaw, or just a sign you're modeling things wrong?"
5+
tags:
6+
["elm", "elm-land", "architecture", "functional-programming", "html-helpers"]
7+
draft: false
8+
---
9+
10+
## The Problem That Shouldn't Exist
11+
12+
Here's a question that comes up occasionally in Elm Land projects: _How do I react when something in the `Shared` model changes?_
13+
14+
It sounds simple enough. You have some global state in `Shared.Model` - maybe feature flags loaded from the backend, authentication status, or some configuration data. Your page needs to _do something_ when that data changes. Not just render differently (that's trivial), but actually perform an effect - fire off a new HTTP request, trigger some side effect locally, or whatever.
15+
16+
And here's where things get interesting: Elm Land doesn't give you a built-in way to do this.
17+
18+
This is either:
19+
20+
1. A legitimate missing feature in Elm Land
21+
2. An architectural flaw in how Elm Land structures applications
22+
3. A sign that you're modeling your state wrong in the first place
23+
24+
Which is it?
25+
26+
## Three Schools of Thought
27+
28+
When this question comes up in the Elm community, you tend to get three different types of responses:
29+
30+
**The Workaround Camp**: "Duplicate the state in your page model and manually diff against `Shared.Model` to detect changes. Or use ports to send messages through JavaScript." Both work, both feel hacky (especially the ports stuff, it gives me the creeps!).
31+
32+
**The Framework Camp**: "This is a known limitation. Elm Land 1.0 will have better support for custom subscriptions - something like a `withOnSharedChange` hook similar to `withOnUrlChange`. But that's not here yet, so in the meantime, don't be afraid to fork the framework and add the hooks you need."
33+
34+
**The Architecture Purist Camp**: "Needing to 'notify' pages of shared state changes is an anti-pattern. Message passing between modules with encapsulated state leads to complexity and tight coupling. The real solution is to model your state differently - use function composition, extensible records, and flatten your state model."
35+
36+
Which camp I'm in is not the point, and I won't tell you (it's the last one, though). But I do like being part of the/a solution, so I found myself being quite the pragmatic when we suddenly faced this issue at my client's.
37+
38+
Lo and behold, the new [`sendMsgWhen`](https://package.elm-lang.org/packages/cekrem/html-helpers/latest/HtmlHelpers#sendMsgWhen) in my `html-helpers` package.
39+
40+
## The new sendMsgWhen helper
41+
42+
Here's how you use it (but, like the box says: I'm not certain you even should...):
43+
44+
```elm
45+
view : Shared.Model -> Model -> View Msg
46+
view shared model =
47+
{ title = "Items Page"
48+
, body =
49+
[ sendMsgWhen (shared.items /= model.prevSharedItems) SharedItemsChanged
50+
, viewItems (model.items ++ shared.items)
51+
]
52+
}
53+
54+
55+
update : Shared.Model -> Msg -> Model -> ( Model, Effect Msg )
56+
update shared msg model =
57+
case msg of
58+
SharedItemsChanged ->
59+
( { model | prevSharedItems = shared.items }, openBannerEffect "New global items, time to celebrate!" )
60+
-- {...other cases}
61+
62+
63+
```
64+
65+
When there's a diff between `shared.items` and `model.prevSharedItems`, a `SharedItemsChanged` message gets sent, and your `update` function can handle it like any other message - fire off a new HTTP request, update local state, whatever you need.
66+
67+
### How It Works (The Clever Hack)
68+
69+
The implementation is extremely hacky:
70+
71+
```elm
72+
sendMsgWhen : Bool -> msg -> Html msg
73+
sendMsgWhen condition msg =
74+
lazyWhen condition
75+
(\() ->
76+
Html.img
77+
[ Attributes.style "display" "none !important"
78+
, Attributes.src "data:,"
79+
, Events.on "load" (Decode.succeed msg)
80+
, Events.on "error" (Decode.succeed msg)
81+
]
82+
[]
83+
)
84+
```
85+
86+
It creates an invisible `<img>` element with an empty data URL. The browser immediately fires either the `load` or `error` event (depending on how well the browser likes the `"data:,"` part), which we catch and turn into our message. It's using the browser's event loop to dispatch a message during rendering.
87+
88+
Is it elegant? No. Is it a proper solution? Definitely not. Does it work? Absolutely.
89+
90+
(You can see the [full source on GitHub](https://github.com/cekrem/html-helpers/blob/9371f55bc11b0f3d9edb579bcb002b7010051b4c/src/HtmlHelpers.elm#L436) if you want to judge me further.)
91+
92+
## The Trade-offs
93+
94+
Let's be honest about what this is:
95+
96+
**Pros:**
97+
98+
- No ports required (stays in pure Elm land)
99+
- No manual diffing in `update` that runs on _every_ message
100+
- Explicit about what changes you're tracking
101+
- Works with the current version of Elm Land
102+
- Doesn't require forking Elm Land
103+
104+
**Cons:**
105+
106+
- Relies on browser implementation details
107+
- Feels hacky (because it is, very much so)
108+
- Sends messages from the view layer (traditionally a no-no!)
109+
- Could be abused if you're not careful (it's important to remember to update that `model.prevSharedItems` entry!)
110+
- Becomes unnecessary when Elm Land 1.0 adds proper hooks?
111+
112+
## When Is This Actually Needed?
113+
114+
Here's the thing I keep coming back to: _How often is this actually even a problem?_
115+
116+
In most Elm Land apps I've worked on, the `Shared` model contains:
117+
118+
- Current user/auth state
119+
- Global UI state (sidebar open/closed, theme, etc.)
120+
- Maybe some cached data
121+
122+
And pages mostly just _read_ from `Shared.Model` to render things differently. They don't need to _react_ to changes with effects.
123+
124+
There _are_ scenarios where the current limitations are a problem, but it's also somewhat rare. And when it does come up, there are often modeling approaches that avoid the whole problem:
125+
126+
1. **Delay initialization**: Don't initialize the page until critical shared data is loaded
127+
2. **Re-fetch on change**: If the data is cheap to fetch, just re-fetch it every time the view renders with new shared data (this is actually fine for many cases)
128+
3. **Model the waiting**: Make your page model explicitly represent the "waiting for shared data" state
129+
130+
These all feel like workarounds too, in their own way. But they're workarounds that push you toward clearer state modeling, which has value.
131+
132+
## The Bigger Question
133+
134+
What bothers me most about this whole situation is the uncertainty. Is needing to react to `Shared` changes a code smell? Or is it a legitimate pattern that frameworks should support?
135+
136+
I'm inclined to agree with my betters who argue that "message passing between modules with encapsulated state is an anti-pattern" and that we should use function composition and extensible records instead. The classic Richard Feldman approach from [Scaling Elm Apps](https://www.youtube.com/watch?v=DoA4Txr4GUs).
137+
138+
## My Current Take
139+
140+
Here's some pragmatic idealism for you:
141+
142+
1. **Most of the time**, if you feel like you need subscriptions to `Shared` changes, you probably need to rethink your state modeling. The Elm Architecture really is powerful enough to handle most cases cleanly.
143+
144+
2. **Sometimes**, you have a legitimate edge case where shared state changes need to trigger effects, and fighting against that creates more complexity than just handling it directly.
145+
146+
3. **Elm Land 1.0** will probably provide better primitives for this (when it arrives), making both the workarounds and some of the modeling gymnastics unnecessary.
147+
148+
In the meantime, I'm okay (I think?) with pragmatic hacks like `sendMsgWhen` for those rare cases where you really need them. But I'm also treating them as a code smell - a sign that maybe I should look harder at my state modeling before reaching for the workaround.
149+
150+
## The Honest (lack of?) Conclusion
151+
152+
I don't have a clean answer here. This isn't a post where I tell you "the right way" to handle this problem (although you might have noticed I've let slip a few hints that this shouldn't even be an issue if we just model our apps right in the first place!).
153+
154+
If you're hitting this issue in your Elm Land app, here are your options:
155+
156+
1. **Rethink your state model** - Maybe you can avoid the problem entirely with better modeling
157+
2. **Wait for Elm Land 1.0** - If you can afford to wait, proper hooks are coming
158+
3. **Use a workaround** - Ports, manual diffing, or `sendMsgWhen` all work
159+
4. **Fork Elm Land** - Add the hooks you need; the framework is designed to be extensible
160+
161+
Each has trade-offs. Each is valid in different contexts. The "right" choice depends on your specific situation, timeline, and tolerance for hackery.
162+
163+
What I _am_ confident about: this is a great example of how framework constraints push us to think harder about our architecture. Even if Elm Land eventually adds `withOnSharedChange`, the conversation about _whether we should even need it_ is valuable. Let's enjoy it and learn from it, in any case!
164+
165+
Sometimes the best solution is a clever workaround. Sometimes it's better modeling. Sometimes it's both. But if you find yourself inventing an event buss using ports or writing code that looks like an Elmish Angular two-way-binding, you probably need to repent and start over :D
166+
167+
And for the record: I really like Elm land!

0 commit comments

Comments
 (0)