Skip to content

Commit 083b291

Browse files
committed
add elm/usereducer draft
1 parent 9517f7b commit 083b291

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
---
2+
title: "The Elm Architecture vs React's useReducer, or 'Same Loop, Different Guarantees'"
3+
date: 2025-09-18
4+
description: "A practical comparison of The Elm Architecture and React's useReducer: how they map conceptually, where they differ in guarantees, and what to watch out for when scaling apps."
5+
tags: ["elm", "react", "usereducer", "architecture", "state-management"]
6+
draft: true
7+
---
8+
9+
I love how often smart engineers arrive at the same shape of solution from different directions. The Elm Architecture (TEA) and React's `useReducer` are a great example: two ecosystems, one mental model.
10+
11+
But this isn't a "which one is better" piece.
12+
13+
Instead, this is a side‑by‑side look at the same loop expressed in two worlds—what maps neatly, what absolutely doesn't, and how the guarantees you get change the way you structure code at scale.
14+
15+
## The Loop We All Build
16+
17+
At heart, both TEA and `useReducer` revolve around a simple loop:
18+
19+
1. A message/action happens
20+
2. You compute a new state
21+
3. You render that state
22+
4. You perform effects that cause more messages/actions
23+
24+
In Elm, this loop is explicit and enforced. In React, it's conventional and flexible.
25+
26+
### Elm
27+
28+
```elm
29+
module Counter exposing (Model, Msg(..), init, update, view, subscriptions)
30+
31+
import Browser
32+
import Html exposing (Html, button, div, text)
33+
import Html.Events exposing (onClick)
34+
35+
type alias Model =
36+
{ count : Int }
37+
38+
type Msg
39+
= Increment
40+
| Decrement
41+
42+
init : () -> ( Model, Cmd Msg )
43+
init _ =
44+
( { count = 0 }, Cmd.none )
45+
46+
update : Msg -> Model -> ( Model, Cmd Msg )
47+
update msg model =
48+
case msg of
49+
Increment ->
50+
( { model | count = model.count + 1 }, Cmd.none )
51+
52+
Decrement ->
53+
( { model | count = model.count - 1 }, Cmd.none )
54+
55+
view : Model -> Html Msg
56+
view model =
57+
div []
58+
[ button [ onClick Decrement ] [ text "-" ]
59+
, div [] [ text (String.fromInt model.count) ]
60+
, button [ onClick Increment ] [ text "+" ]
61+
]
62+
63+
subscriptions : Model -> Sub Msg
64+
subscriptions _ = Sub.none
65+
```
66+
67+
### React with useReducer
68+
69+
```jsx
70+
import { useReducer } from "react";
71+
72+
const initialState = { count: 0 };
73+
74+
const Action = {
75+
Increment: { type: "Increment" },
76+
Decrement: { type: "Decrement" },
77+
};
78+
79+
const reducer = (state, action) => {
80+
switch (action.type) {
81+
case "Increment":
82+
return { ...state, count: state.count + 1 };
83+
case "Decrement":
84+
return { ...state, count: state.count - 1 };
85+
default:
86+
return state;
87+
}
88+
};
89+
90+
export const Counter = () => {
91+
const [state, dispatch] = useReducer(reducer, initialState);
92+
93+
return (
94+
<div>
95+
<button onClick={() => dispatch(Action.Decrement)}>-</button>
96+
<div>{state.count}</div>
97+
<button onClick={() => dispatch(Action.Increment)}>+</button>
98+
</div>
99+
);
100+
};
101+
```
102+
103+
Same idea. Different enforcement.
104+
105+
## Effects: `Cmd` vs `useEffect`
106+
107+
Effects are where the similarities carry you 80% of the way and then the last 20% decides your production story.
108+
109+
- **Elm `Cmd Msg`**: declarative, described in pure code, executed by the runtime, results come back as `Msg` via the same update loop.
110+
- **React `useEffect`**: imperative side‑effects run after render; you orchestrate async work yourself and call `dispatch` when ready.
111+
112+
### Elm: fetch on click
113+
114+
```elm
115+
type Msg
116+
= Fetch
117+
| Fetched (Result Http.Error String)
118+
119+
update : Msg -> Model -> ( Model, Cmd Msg )
120+
update msg model =
121+
case msg of
122+
Fetch ->
123+
( model
124+
, Http.get
125+
{ url = "/api/greeting"
126+
, expect = Http.expectString Fetched
127+
}
128+
)
129+
130+
Fetched (Ok txt) ->
131+
( { model | greeting = txt }, Cmd.none )
132+
133+
Fetched (Err _) ->
134+
( { model | greeting = "Oops" }, Cmd.none )
135+
```
136+
137+
No mutation leaks, no forgotten cleanup. The runtime guarantees ordering and cancellation semantics.
138+
139+
### React: fetch on click
140+
141+
```jsx
142+
const reducer = (state, action) => {
143+
switch (action.type) {
144+
case "Fetch":
145+
return { ...state, loading: true };
146+
case "Fetched":
147+
return { ...state, loading: false, greeting: action.payload };
148+
case "FetchFailed":
149+
return { ...state, loading: false, error: true };
150+
default:
151+
return state;
152+
}
153+
};
154+
155+
const fetchGreeting = async (dispatch, signal) => {
156+
try {
157+
const res = await fetch("/api/greeting", { signal });
158+
const txt = await res.text();
159+
dispatch({ type: "Fetched", payload: txt });
160+
} catch (e) {
161+
if (e.name !== "AbortError") dispatch({ type: "FetchFailed" });
162+
}
163+
};
164+
165+
export const Greeting = () => {
166+
const [state, dispatch] = useReducer(reducer, {
167+
loading: false,
168+
greeting: "",
169+
error: false,
170+
});
171+
172+
useEffect(() => {
173+
if (!state.loading) return;
174+
const controller = new AbortController();
175+
fetchGreeting(dispatch, controller.signal);
176+
return () => controller.abort();
177+
}, [state.loading]);
178+
179+
return (
180+
<button onClick={() => dispatch({ type: "Fetch" })}>
181+
{state.loading ? "Loading..." : state.greeting || "Load"}
182+
</button>
183+
);
184+
};
185+
```
186+
187+
It works well—but the discipline is on you. Cleanup, idempotency, and avoiding render‑effect feedback loops are not enforced by the framework.
188+
189+
## Where They Map 1:1
190+
191+
- **Model/State**: Elm `Model` ↔ React reducer state.
192+
- **Msg/Action**: Elm `Msg` union ↔ React action objects.
193+
- **Update/Reducer**: Pure function, input is message/action and old state; output is new state.
194+
- **View**: Elm `Html Msg` ↔ React JSX with `dispatch` passed through.
195+
196+
If you stick to those, `useReducer` feels very Elm‑ish.
197+
198+
## Where Guarantees Diverge
199+
200+
- **Type system**: Elm's `Msg` is an exhaustive union with compiler‑enforced handling. React needs TypeScript and discipline; you can still forget cases or widen to `any`.
201+
- **Side‑effects**: Elm turns effects into data (`Cmd`) processed by the runtime. React executes effects directly in `useEffect`, making ordering, cancellation and concurrency your responsibility.
202+
- **Global invariants**: Elm prevents invalid states by construction ("make impossible states impossible"). React can emulate via types and state modeling, but nothing stops you from sprinkling additional `useState` that bypasses your reducer.
203+
- **Refactoring safety**: Elm's compiler becomes your pair programmer. In React, the compiler helps if you invest in strict TypeScript and ESLint rules.
204+
- **Debugging/time travel**: Elm debugger is built‑in. React has excellent devtools but time travel depends on external tooling and reducer purity.
205+
206+
## Scaling the Codebase
207+
208+
Both approaches scale, but the pressure points differ.
209+
210+
- **Elm**
211+
212+
- Break large modules into feature modules with their own `Model/Msg/update/view` and compose.
213+
- Use the pattern championed by Richard Feldman (see the `elm-spa-example`) to keep message routing explicit.
214+
- JSON decoding/encoding is explicit; friction up‑front, fewer surprises later.
215+
216+
- **React**
217+
- Co‑locate reducers per feature; lift them into context providers when shared.
218+
- Guard against reducer bypass: prefer reducer‑only state for domain data, `useState` only for purely local UI details.
219+
- Encapsulate effects in custom hooks that dispatch actions, mirroring Elm `Cmd` modules.
220+
221+
## Common Foot‑guns (and How to Avoid Them)
222+
223+
- **Effect leaks**
224+
225+
- Elm: runtime handles cancellation; your job is to model messages.
226+
- React: always return a cleanup from `useEffect`, and use `AbortController` for fetch.
227+
228+
- **Invalid states**
229+
230+
- Elm: encode impossible states in types (e.g., `type RemoteData a = NotAsked | Loading | Success a | Failure`).
231+
- React: mirror that with discriminated unions in TypeScript and a single reducer.
232+
233+
- **Reducer escape hatches**
234+
- Elm: not possible; all state changes flow through `update`.
235+
- React: avoid mixing `useState` that mutates the same domain; route through `dispatch`.
236+
237+
## Choosing for a Project
238+
239+
- **Choose Elm when** you want strong guarantees, explicitness by default, and a runtime that enforces the architecture. The learning curve is front‑loaded; the payoff is in refactoring safety and fewer production surprises.
240+
241+
- **Choose React + useReducer when** you need to integrate with a large React ecosystem, want flexibility, and are willing to enforce constraints via conventions, TypeScript, and linting.
242+
243+
Both can yield beautiful, maintainable systems if you design with the loop in mind and treat effects as first‑class concerns.
244+
245+
## A Handy Mapping Table
246+
247+
- **Elm `Model`**: React reducer state
248+
- **Elm `Msg`**: React action (prefer discriminated unions in TS)
249+
- **Elm `update`**: React reducer function
250+
- **Elm `Cmd`**: React `useEffect` + async function that dispatches
251+
- **Elm `subscriptions`**: React event sources (intervals, websockets) wrapped in `useEffect`
252+
- **Elm ports**: Boundary to JS world ↔ React imperative escape hatches (but keep them at the edges)
253+
254+
## Closing Thought
255+
256+
Same loop. Different guarantees. If you adopt Elm's discipline inside React—single reducer per domain, discriminated unions, effects as data‑producers that funnel back through `dispatch`—you get most of TEA's clarity. If you adopt React's pragmatism inside Elm—clear module boundaries and small, composable features—you get most of the ergonomics people love about React.
257+
258+
Either way, design for change. Future‑you will thank present‑you.
259+
260+
---
261+
262+
_Further reading: Richard Feldman's `elm-spa-example` for project organization, and the React docs on `useReducer` and `useEffect` for effect modeling._

0 commit comments

Comments
 (0)