Introduction

If you’ve spent any time at all using React, chances are you’ve felt both the framework’s power and its complexity. Hooks are flexible, TypeScript catches many errors. But runtime surprises still slip through. useEffect dependencies are easy to mess up, performance bugs eventually demand quite strategic (and specific!) React.memo and useCallback usage, and what started out quite harmlessly easily gets out of hand. I don’t think these are signs you’re doing React wrong; they’re the natural trade-offs of React’s flexibility.

Some of my most-read posts are the deep-dive React articles (especially on React.memo and reconciliation). A few are used in React courses, and some have even been translated to Korean(!). This popularity reflects a truth: once you step beyond the basics, React’s complexity increases dramatically and the good can become challenging faster than you expect.

Elm takes a different approach. It’s a small functional language with one way to build applications, designed for reliability and clarity. With Elm, you trade flexibility for focus. There’s exactly one architecture (The Elm Architecture, or TEA), exactly one way to represent side effects, and a type system that works hard to prevent runtime errors. The result is a developer experience that feels, at least at first, strangely quiet. You don’t spend hours debugging why your component didn’t re-render or why state mutated unexpectedly. Instead, you spend time modeling your problem domain.

For React developers, the first encounter with Elm can feel almost too simple. There are no hooks to juggle, no contexts to configure, no class vs. function components. Just a Model, an Update function, and a View. And yet, that simplicity scales to codebases with hundreds of thousands of lines.

So why consider Elm if you already know React?

  • To catch more errors at compile time. Many React/TypeScript errors show up only in the browser. Elm’s compiler is famously strict: if it compiles, it’s very likely to work.
  • To simplify mental overhead. Fewer concepts mean fewer ways to get things wrong. Instead of choosing between a dozen state management solutions, Elm gives you one–TEA.
  • Because it’s actually fun. Elm is genuinely enjoyable to work with. The compiler points you toward fixes, the language encourages clear modeling, and your app tends to just work once it compiles.

But there’s a deeper reason to learn Elm, even if you never use it professionally: it’s the fastest way to truly learn functional programming. Real functional programming, the kind that actually changes how you think about code in any language. Haskell has too many language features and quirks, making it notoriously hard to get started with something practical. Elm, by contrast, is remarkably small and focused (the entire language fits in your head). And crucially, you’re working in a domain you already know: building web UIs. Strict FP discipline plus a domain you already know–that’s a surprisingly good combo for actually learning this stuff.

If you’ve ever wanted to grow as a developer by learning functional programming, Elm is in my honest opinion your shortest path. By far! Especially if you’re already familiar with React.

This book is a guided tour for React developers: what Elm looks like, how it compares, and how you might start using Elm–even if only as a single widget inside a React app. By the end, you should have a feel for whether Elm can play a role in your work. Better yet: you will have learned functional programming along the way! Whether you end up actually abandoning React is beside the point (and probably not all up to you).

If React has been your main frontend toolkit, Elm shows you what frontend development feels like with different trade-offs. You get to see what it looks like to write only functional code, and to keep all side effects at the boundary where it belongs. It may not be for every project, but once you’ve experienced Elm’s compiler, you’ll wish more of your codebases worked this way.

And learning Elm will be worthwhile whether you end up using it professionally or not. Especially in this day and age, learning something transferable (not just syntax and recipes) that helps us solve real engineering problems is very valuable.

To give you a taste of what I mean, let me show you the kind of guarantees Elm provides. Throughout this book, you’ll see what I call “Elm Hooks” in each chapter–quick previews of how Elm handles that chapter’s topic. React has hooks for state management, but Elm has hooks that keep you hooked on the language itself.

Try this one. It’ll demonstrate two of Elm’s main selling points: the friendly compiler and the strict type system (or, good cop / bad cop, if you prefer).

An icon of a info-circle1

Elm Hook

Given the following Elm code

 1 type Animal = Cat | Dog | Penguin
 2 
 3 
 4 animalToString : Animal -> String
 5 animalToString animal =
 6     case animal of
 7         Dog ->
 8             "dog"
 9 
10         Cat ->
11             "cat"
12 
13         -- Ooops, forgot about that Penguin!

The compiler answers the following:

 1 -- MISSING PATTERNS --------------- /path-to-your/src/Main.elm
 2 This `case` does not have branches for all possibilities:
 3 
 4 306|>    case animal of
 5 307|>        Dog ->
 6 308|>            "dog"
 7 309|>
 8 310|>        Cat ->
 9 311|>            "cat"
10 
11 Missing possibilities include:
12 
13     Penguin
14 
15 I would have to crash if I saw one of those. Add branches for them!
16 
17 Hint: If you want to write the code for each branch later, use `Debug.todo` as a
18 placeholder. Read <https://elm-lang.org/0.19.1/missing-patterns> for more
19 guidance on this workflow.

Sure beats all those ensureNever helpers you’ve written in TypeScript, huh?