React's Evolution Toward Elm
React's Evolution Toward Elm
A history of how React, originally a class-based component framework, gradually adopted The Elm Architecture patterns. Most React developers don't know they're writing Elm-flavoured code; the lineage is real and worth understanding.
The story is roughly: React started imperative-with-classes (2013); Redux brought reducers (2015); Hooks made functional components mainstream (2018); useReducer normalised the pattern (2018); modern React-with-server-components is moving even further toward declarative + immutable.
By 2026, idiomatic React is closer to Elm than to its own 2015 self.
Phase 1: Class components (2013–2018)
Original React. Stateful components held mutable state in this.state; updates via this.setState:
class Counter extends React.Component {
constructor() {
super();
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<button onClick={this.increment}>
{this.state.count}
</button>
);
}
}
Component-local state. Mutations via callbacks. View was a method on the class. Lifecycle methods (componentDidMount, componentWillUpdate) for side effects. Familiar to anyone who'd written GUIs in OO languages — and increasingly clunky as apps grew.
Phase 2: Redux (2015)
Dan Abramov released Redux at React Europe 2015, demoing time-travel debugging. Redux pulled state out of components into a single global store, mutated only via reducers.
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case "INCREMENT": return { count: state.count + 1 };
case "DECREMENT": return { count: state.count - 1 };
default: return state;
}
}
const store = createStore(counterReducer);
store.dispatch({ type: "INCREMENT" });
console.log(store.getState()); // { count: 1 }
This was The Elm Architecture in JS, openly. Dan's original talk credits Elm directly. Time-travel debugging was the demo; the architecture was the lasting contribution.
For five years, "React + Redux" was the default stack for non-trivial React apps. Boilerplate was high but the discipline was real.
Phase 3: Hooks (2018)
React 16.8 introduced hooks — functions like useState, useEffect, useReducer that let function components hold state and side effects without classes. The big change: function components became the default; classes deprecated in spirit.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
useState hides a reducer behind a tuple-returning function. useReducer exposes the reducer directly:
function counterReducer(state, action) {
switch (action.type) {
case "INCREMENT": return { count: state.count + 1 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<button onClick={() => dispatch({ type: "INCREMENT" })}>
{state.count}
</button>
);
}
Same shape as Elm/Redux. Smaller scope (per-component vs global). The pattern is now built into React itself.
Phase 4: useEffect (2018)
useEffect is React's Cmd equivalent — declarative side effects keyed to state changes:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(u => { if (!cancelled) setUser(u); });
return () => { cancelled = true; }; // cleanup
}, [userId]); // re-run when userId changes
return user ? <div>{user.name}</div> : <div>Loading…</div>;
}
Less pure than Elm's Cmd (the side effect runs inline rather than being returned as data), but the intent is the same: declare what side effects should happen given the current state, let React orchestrate them.
The cleanup function pattern handles "cancellation when state changes" — Elm subscriptions do something similar. The compositional bite is similar.
Phase 5: Server components and Suspense (2022–)
React Server Components, Suspense for data fetching, and the Next.js App Router push further toward declarative-fetching:
async function UserProfile({ userId }) {
const user = await fetchUser(userId);
return <div>{user.name}</div>;
}
The component is async; React suspends rendering until the data arrives. No useEffect, no useState. Closer to "view IS a function of inputs" than React has ever been.
What's still missing relative to Elm
React isn't Elm. Five gaps:
- No type safety guarantees. TypeScript helps, but React still allows mutation, runtime errors, missing dependencies. Elm's compiler is stricter.
- Hooks have rules ("only call hooks at the top level of components"). Elm's runtime has no such gotchas.
- State is per-component by default. Lifting state up is awkward. Elm's single Model is simpler architecturally (and harder for casual use).
- Side effects are inline.
useEffectcalls fetch directly. Elm's Cmd returns a description; the runtime fetches. - Concurrency model is opaque. React's "concurrent mode" is hidden behind hooks; Elm's runtime is explicit.
Modern React is "Elm-flavoured" but not "Elm-strict." That's intentional — React optimises for adoption breadth. Elm optimises for guarantees.
Why this history matters
If you came to programming via React, The Elm Architecture feels like "Redux but in a different language." That's roughly right.
If you understand TEA from Elm, modern React makes more sense — the patterns React stumbled into over a decade are the patterns Elm shipped on day one.
If you're building reactive UIs in any framework that has reducers + a render function (Bubbletea, tui-realm, ratatui-with-Elm-discipline, Lazydap's TUI), you're on the same architectural path. The names differ; the shape is the same.
What to remember
- "Redux" is "Elm" with worse names and more boilerplate (in 2015, anyway).
- "useReducer" is "make Elm's update function explicit in React."
- "useEffect" is "make Elm's Cmd subscriptions inline in React."
- React's trajectory has been toward more-Elm, not less.
- The pattern is universal; the language is incidental.
See also
- The Elm Architecture — what React converged toward
- Reducers — the generalised name for Update
- Side Effects via Cmd — what useEffect partially is
- Pure Functions and Immutable State — the foundation
- Elm (the language) — where the patterns came from
- MVU in Other Ecosystems — the same pattern in Bubbletea, tui-realm, Compose, etc.
- Dan Abramov's "Live React: Hot Reloading with Time Travel" (2015): https://www.youtube.com/watch?v=xsSnOQynTHs
- React's hooks introduction (2018): https://reactjs.org/docs/hooks-intro.html