React has the notion of stateless and stateful components. So naturally, when frontend developers who are familiar with those concepts start learning Elm they want to know how to build those types of components with Elm. Unsurprisingly, they are baffled when we tell them that it's not useful to think in terms of components in Elm. Components are objects and objects don't exist in Elm.
So, how do you compose components?
There are no components in Elm. They don't exist because objects don't exist in Elm.
There are no components?
It must be impossible to write component libraries then?
Nope! It's possible. Allow me to explain.
Table of Contents
- Components
- Stateless components in React
- Stateful components in React
- Reusable views in Elm
- How to build something like
Greeting
in Elm? - How to build something like
Counter
in Elm? - How to use
Counter
? - A way forward
- View state is rare
- View state is view dependent
- View state does not imply nested TEA
- Revisited: How to build something like
Counter
in Elm? - Conclusion
- Learn more
- References
Components
Firstly, I encourage you to read Components written by Evan in the Elm guide.
On mental models:
Mental models are how we think something works.
A great deal of understanding rests in getting a few small details right. And once the basics are right, additional knowledge often changes very little.
Because of this, the key to understanding something is often getting the basic model right.
The brain's favorite method for building models is to take parts from something else it already understands.
On mental sets:
A mental set is a tendency to only see solutions that have worked in the past. This type of fixed thinking can make it difficult to come up with solutions and can impede the problem-solving process.
Mental sets can lead to rigid thinking and create difficulties in the problem-solving process.
So here's my understanding of the component dilemma.
You're new to Elm and you're wondering, hmm, "How do I build my components so that I can reuse them as needed?" The logic is sound because, in React, there are these things called components and you know how to work with them. If you can just figure out how to do components in Elm then you'd be good to go. So you're trying to build your mental model of view reuse in Elm by taking parts from something you already understand how to do in React. That's reasonable. However, since a great deal of understanding rests in getting a few small details right we need to let you know as soon as possible that this way of thinking about reusing views, where everything is a component, isn't going to be a useful way of thinking in Elm. That's why Evan emphatically says "Actively trying to make components is a recipe for disaster in Elm."
The other thing that's happening is that there's variable shadowing in English. The word "component" is overloaded with many meanings. As a result, we have to rely on contextual clues to decipher its meaning. In the world of frontend web development the word has become synonymous with React component and, since React is developed in a language that supports mutable state, methods, and objects, it has become commingled with those concepts as well.
I know, many people use "component" to mean what it used to mean back in the day which was "A part that combines with other parts to form something bigger." Huh? What it used to mean back in the day? I mean it still means that, it's an alternative definition. I'm sure if I ask someone with no knowledge of frontend web development what they think component means this is definitely the meaning they're going to imply. In fact, when I use the term I also imply that as well.
An aside: This is one reason why variable shadowing isn't allowed in Elm. It just leads to unnecessary confusion.
Let me attempt to summarize. The word "component" strongly implies React component in the context of frontend web development. It is also strongly correlated with mutable local state and methods since JavaScript allows that. As a consequence, Evan takes the stance that "component" means "local state + methods" and doesn't consider the other meanings because variable shadowing leads to confusion. With that definition of "component" it becomes harmful to think in terms of components because you'd be thinking in terms of objects and well, objects don't exist in Elm. Hence, the "everything is a component" mental model is not a mental model we want you to adopt when thinking about view reuse in Elm.
Whew! I hope that helps. Let me know in the comments if you need me to explain it further.
With that out of the way let me assure you that if what you're asking about when you ask about components is whether or not you can decompose your web application into parts so that you can reuse those parts to form something bigger then the answer is a resounding YES and we call it making reusable views.
Stateless components in React
Stateless components in React are components that do not have any state. Their main purpose is to render the UI based on the props passed to them.
For instance, here's a simple example of a stateless component:
export const Greeting = ({ name }) => {
return <h1>Hello, {name}!</h1>;
};
Stateful components in React
Stateful components in React have state and are responsible for managing and updating that state.
For instance, here's a simple example of a stateful component:
import { useState } from 'react';
export const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount((c) => c + 1);
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
</div>
);
};
Reusable views in Elm
React is a JavaScript library for building user interfaces.
Elm is a programming language. So it doesn't make sense to compare Elm and React. It makes more sense to compare Elm and JavaScript.
So what do we use for building user interfaces in Elm?
We use a library called elm/html
. Like React, it is based on the virtual DOM concept.
elm/html
exports the type Html msg
which represents the type of values which you can use to describe your HTML content.
A view in Elm is any function that returns Html msg
.
In particular, a view is a function.
Functions in any programming language, especially in functional languages, especially in Elm, are the building blocks that allow you to write reusable code.
Functions are reusable. Views are functions. So, a reusable view is actually nothing special. It's just another name for a view.
How to build something like Greeting
in Elm?
module Main exposing (main)
import Html as H
main : H.Html msg
main =
viewGreeting "Elm"
viewGreeting : String -> H.Html msg
viewGreeting name =
H.h1 [] [ H.text ("Hello, " ++ name ++ "!") ]
How to build something like Counter
in Elm?
module Counter exposing (Model, init, Msg, update, view)
import Html as H
import Html.Events as HE
-- MODEL
type Model
= State Int
init : Model
init =
State 0
-- UPDATE
type Msg
= ClickedIncrement
update : Msg -> Model -> Model
update msg (State n) =
case msg of
ClickedIncrement ->
State (n + 1)
-- VIEW
view : Model -> H.Html Msg
view (State n) =
H.div []
[ H.h1 [] [ H.text ("Count: " ++ String.fromInt n) ]
, H.button
[ HE.onClick ClickedIncrement ]
[ H.text "Increment" ]
]
This approach is called nested TEA because it mimics the Elm architecture within the module by breaking it into the three core parts of the Elm architecture, namely MODEL, VIEW, and UPDATE.
How to use Counter
?
To use the Counter
module we'd have to wire it up in Main
as follows:
module Main exposing (main)
import Browser
import Counter
import Html as H
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
-- MODEL
type alias Model =
{ counterModel : Counter.Model
}
init : Model
init =
{ counterModel = Counter.init
}
-- UPDATE
type Msg
= ChangedCounterModel Counter.Msg
update : Msg -> Model -> Model
update msg model =
case msg of
ChangedCounterModel counterMsg ->
{ model | counterModel = Counter.update counterMsg model.counterModel }
-- VIEW
view : Model -> H.Html Msg
view { counterModel } =
H.map ChangedCounterModel (Counter.view counterModel)
To use the corresponding component in React you don't have to deal with all that wiring. You'd just do <Counter />
.
The key takeaway here is that React makes it super easy to use stateful components and Elm makes it quite tedious to use nested TEA which gives you something similar. I'm sure you can begin to see that if you decide to build all your views in this way, i.e. using nested TEA, you'd have a lot of wiring to deal with.
So, the Counter
module is proof that Elm allows you to build something like the stateful components you see in React. Unfortunately, that way of compartmentalization requires writing a lot of boilerplate. Furthermore, there's no way around the boilerplate because Elm doesn't have global mutable state. The only way to change state is by going through the Elm runtime. You get access to the Elm runtime by using one of Browser.sandbox
, Browser.element
, Browser.document
, or Browser.application
. And, to change your state, you have to arrange to execute your code in the update
function you provide to Browser.sandbox
and friends.
This is why naively copying ideas from React will lead to painful Elm code. React and JavaScript have features that make their way of building user interfaces a nice experience. Whereas, Elm has features that make an alternative way of building user interfaces a nice experience. But to appreciate the Elm approach, you have to part ways with the "everything is a component" mindset and embrace a different mindset.
What mindset then?
One based on functions, modules, and custom types.
A way forward
All states are not created equal. We can partition state into application state and view state.
I like to define application state as any state that's not view state. So what's view state?
View state is any state that is inherent to the display and operation of the user interface element. No other part of the user interface cares about a given element's view state.
In Elm, it is important to discover early on which user interface elements require view state.
Why?
Because, only user interface elements that require view state demand the nested TEA approach.
Let me repeat that.
ONLY USER INTERFACE ELEMENTS THAT REQUIRE VIEW STATE DEMAND THE NESTED TEA APPROACH.
All other user interface elements could be built with view functions alone and zero nested TEA.
Application state would live in either the main model or a page's model.
Let me illustrate what I mean with some examples.
Example: Super Rentals
Super Rentals is an Elm implementation of the Ember Tutorial's "Super Rentals" web application.
- All application state lives in
Main
or in one of the page modules. - None of the reusable views required view state and so none of the view modules used the nested TEA approach.
Example: Conduit
Conduit is an Elm SPA for RealWorld's Medium.com clone.
- All application state lives in
Main
or in one of the page modules. - None of the reusable views required view state and so none of the view modules used the nested TEA approach.
An aside: mindplay-dk
opened an issue, Making RealWorld “realer” (2.0?), in gothinkster/realworld
commenting on the fact that the Conduit frontend does not present any use case for any kind of user interface control with internal or accidental state. In other words, he noticed that none of the user interface controls required view state. This correlates with the fact that we didn't need to use nested TEA to implement the reusable views in Conduit.
View state is rare
I've built quite a few Elm apps now and what I've come to realize is that view state is quite rare. As a result, nested TEA is rarely required.
Let's backtrack. Remember how we tried to mimic the stateful counter component from the React example using nested TEA. Imagine if we went down that route for every view we had in our application. We would have ended up with a ton of boilerplate. That would have been very unfortunate because we would have made nested TEA a necessary part of our application when in most cases it's completely unnecessary.
Evan, Richard, and countless others have experienced the pain of trying to mimic stateful components in Elm with nested TEA and that's why they are warning us not to take that route from the start.
Only reach for nested TEA when you have no other alternatives.
But, do you have any examples where using nested TEA was a good tradeoff?
I sure do.
Example: 2048
2048 is an Elm clone of Gabriele Circulli's JavaScript version of the game.
- All application state lives in
Main
andView.Main
. - Of the 9 reusable views, 2 of them used nested TEA.
Example: 7GUIs
7GUIs defines seven tasks that represent typical challenges in GUI programming.
-
Task.CircleDrawer.View.Dialog
uses nested TEA. -
Task.Cells.View.Sheet
uses nested TEA.
Other examples
In applications that I've written for work I've also used nested TEA quite infrequently but it has come up. I've used it for building navigations, modals, and form controls.
There has only ever been one instance, for me, where nested TEA was utterly annoying to deal with. It came up when I was building the Qoda DAO. When you log into the Qoda DAO it uses your wallet address to get information about your rewards. Your rewards can change over time. So, while you're logged in, in order to signal to you that your data changed, we highlight the changed values and then fade them back to their original color after a few milliseconds. The view that does it requires view state and to implement it I needed to use nested TEA. However, this view appears all over the place within the page and it uses data from the blockchain that may or may not be available. As a result the wiring becomes insane because there are over 10 pieces of data that could change if it existed on the page. Suffice it to say, I used a web component to side-step all that complexity.
After that experience, I now recommend using web components for your reusable views that have view state, that must be implemented with nested TEA, and that is reused in an inconsistent way across a page. If it's used in a consistent way, for e.g. you have a list of them, then that's really no trouble to work with. It's when it's used all over the place and depends on other factors that it becomes troublesome to manage. I suspect that that is an even rarer situation.
Web components
In general, web components can also be an option for complex user interface elements because after you invest the time to build them you might want to reuse them in other places besides your Elm web application.
View state is view dependent
It is important to realize that you can easily tell ahead of time if any of your views would require view state by interrogating your user interface elements.
View state does not depend on the size of your application. View state does not depend on the complexity of your business logic. View state only depends on a given user interface element.
View state does not imply nested TEA
Remember when I said "Only user interface elements that require view state demand the nested TEA approach." Well, they may demand it, like an unruly child, but we don't have to give in.
If you have view state you don't necessarily have to use the nested TEA approach. What this means is that there are actually ways to structure your views that have view state such that you don't end up using nested TEA to implement them.
The classic example is Evan's elm-sortable-table
. The repository may be deprecated but the ideas that it contains are still valuable and worth learning about.
Another excellent example is Abadi Kurniawan's datetimepicker
.
Yet another example is my elm-rater.
I hope you're beginning to see that nested TEA is definitely rarely needed. Because, even when there is view state you've now learned that you can still avoid nested TEA.
Revisited: How to build something like Counter
in Elm?
Maybe we're prematurely tying counter to a particular view. Let's think about the counter independent of how it looks. We want to be able to create a counter starting at 0. We want to be able to increment it. We want whatever is going to display the counter to be able to get the current value of the counter.
module Data.Counter exposing (Counter, zero, increment, toInt, toString)
type Counter
= Counter Int
zero : Counter
zero =
Counter 0
increment : Counter -> Counter
increment (Counter n) =
Counter (n + 1)
toInt : Counter -> Int
toInt (Counter n) =
n
toString : Counter -> String
toString =
String.fromInt << toInt
Now we can unit test counter independent of any user interface representation we decide to give it.
Suppose we've settled on the one from the example. Then, we can implement that as follows:
module View.Counter exposing (view)
import Data.Counter as Counter exposing (Counter)
view : Counter -> msg -> H.Html msg
view counter onIncrement =
H.div []
[ H.h1 [] [ H.text ("Count: " ++ Counter.toString counter) ]
, H.button
[ HE.onClick onIncrement ]
[ H.text "Increment" ]
]
Notice how we've extracted a Counter
data type and separated concerns. Data.Counter
is solely responsible for the business logic of counting whereas View.Counter
is responsible for user interface stuff. When your designer comes along and designs a nicer view for your counter you don't have to touch your business logic. Just as it should be.
To use we don't have to worry about any nested update functions or opaque message types. There's much less wiring involved.
module Main exposing (main)
import Browser
import Data.Counter as Counter exposing (Counter)
import Html as H
import View.Counter
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
type alias Model =
{ counter : Counter
}
init : Model
init =
{ counter = Counter.zero
}
type Msg
= ClickedIncrement
update : Msg -> Model -> Model
update msg model =
case msg of
ClickedIncrement ->
{ model | counter = Counter.increment model.counter }
view : Model -> H.Html Msg
view { counter } =
View.Counter.view counter ClickedIncrement
It's actually more code for the simple use case but there's going to be less wiring involved and less code in the long run if you reuse the counter a lot. At the same time, it's not about less or more code, it's about the architecture that evolves over time as you continue to apply this approach. This approach pushes you to figure out how to make the best use of functions, modules, and custom types.
Conclusion
Elm has functions, modules, custom types and a few other features that allow you to write modular reusable code. The Elm architecture (TEA) is the overarching design pattern that shapes the boundary of your Elm application. It provides an adapter to the Elm runtime that you must plug into to give your application life. However, you must not let it dictate the architecture of your entire application.
Breaking down your application into reusable independent parts is fundamental to controlling complexity as your application grows. React wants you to think that everything is a component because that is its main way of decomposition and reuse. React is trying to improve upon some of JavaScript's shortcomings as a programming language. Elm on the other hand was designed from the ground-up to bake in time tested features that are excellent at decomposition and reuse. You don't have to think that everything is a component in Elm and as I've explained it can actually be quite disadvantageous to think that way in Elm.
If you want to reuse a view, start with a function. If it's too specific, abstract it further. Use your entire bag of functional programming tricks to make your functions reusable.
Maybe you want to reuse your view within various files and not just within the file in which it is defined. Then, reach for modules. Those modules don't need to mimic TEA. Those modules don't need to know about TEA at all.
Maybe your view makes use of state that you don't want anyone else touching. Combining modules with custom types allows you to create opaque types. You can use opaque types to hide details. No one would be able to touch what you don't want them to touch. It all depends on the API you expose from your module.
So, no! There aren't any stateless or stateful components in Elm. There are only functions, modules, and custom types and they allow you to make reusable views.
Learn more
These resources can help you change your mental models and mental sets when it comes to thinking about ways to approach building Elm web applications.
- Scaling Elm Apps from Elm Europe 2017
- Scaling Elm Apps from Elm Radio Episode #19
- The Life of a File from Elm Europe 2017
- The Life of a File from Elm Radio Episode #14
- Make Data Structures from Elm Europe 2018
- Making Impossible States Impossible from elm-conf 2016
- Domain Modeling Made Functional
References
- Stateless vs stateful components
- Why can't we create a stateful component?
- Evan's thoughts on components from the Elm guide
- Mental Models
- How Mental Sets Can Prohibit Problem Solving
- The Cambridge Dictionary's definition of "component"
- A discussion about variable shadowing being disallowed in Elm 0.19
elm/html
-
Html msg
- The nested Elm architecture
- Structuring Web Apps from the Elm guide
- Lit - A library that helps you build native web components
- Vincent Navetat shares how he wrote a web component to build a simple tooltip component that never goes off screen and how it was used in Elm
Subscribe to my newsletter
If you're interested in improving your skills with Elm then I invite you to subscribe to my newsletter, Elm with Dwayne. To learn more about it, please read this announcement.
Top comments (8)
I admit I expected something less interesting from the title, as the ideas it hints at are quite well understood by now, but this summary is still worth reading.
I don't agree with the conclusions, and that's one of the reasons I've removed Elm from my toolbox. For starters, in the kind of application I needed to develop what you refer to as "view state" was very pervasive and the problems you underline much more exacerbated. I feel like the 'one
Model
' approach starts to creak and grate if I have to artificially separate it between interesting and uninteresting data every time.Similarly, I've found that nesting TEA is not necessary only as long as the application is simple. Sure, I can keep
Model
andMessage
flat even with dozens of fields/variants, but the code becomes hard to navigate. Tiny little frictions that make the whole experience unpleasant.Most importantly I was irked by the general response of the community to those issues, which has always been "they're not really issues, you're just looking at it wrong!". I have only developed a handful of applications in Elm, but I always felt the need to nest TEA somehow, due to heavily separated pages with a lot of view state. At some point I decided to circumvent the problem by venturing outside Elm, just like you suggest with web components. It works, but it's somewhat of a taboo in the community. Even here you just name the feature without delving any further into how to actually implement it. The whole thing is brushed aside as if it wasn't an actual issue, and I guess maybe it isn't if you only stick to a certain class of projects - which is not what I needed Elm for.
Firstly, thanks for taking the time to share your honest opinion. I would very much like to understand your perspective so we can see if there's a way forward.
What kind of application did you need to develop?
I haven't experienced it being a taboo in the community so I can't say much more to that because it would just be my experience versus your experience and both experiences can be valid.
It would have been too much of a tangent. I do have plans to go into it in future articles though. Stay tuned!
At my job we are going to use web components to allow us to use some of our views in our Elm web applications and our non-Elm static websites. I would also share how that went once I get it done.
Off-topic:
I learned to build websites a long time ago before web frameworks were so prevalent. Back then, you'd slice PSDs and build everything out in HTML, CSS with a sprinkle of JavaScript where needed. Even now when I build Elm web applications I follow the same process. Only now the designs are given to me in Figma and not Photoshop. I build every view in the design from the ground-up using just HTML, CSS, and JavaScript. This means when I'm done I can choose to implement it in React, Vue.js, AngularJS, elm-html, elm-html + web components. It doesn't matter. You just learn the peculiarities of the view library and you translate from the HTML, CSS, and JavaScript to that view library. For the Elm side of things I can see before I even implement a line of Elm code which views are going to require view state. At this point I have a choice, use Elm with nested TEA or use web components. That's a 100% local technical decision I have to make based on the tradeoffs I see in my project at that point in time. It has nothing to do with whether or not web components are taboo in the community. It's my project and going with web components actually decouples me from Elm which is a good thing.
But now you may ask. Why choose Elm then? And, I've thought about this a lot as well. If your views can now be built in web components which decouple you from Elm then what's left? What's left is business logic. And, if your business logic is sufficiently complex then a strong case can still be made for keeping Elm in your stack. When the React community faces problems with scaling it's usually the business logic aspects that challenge them. They start reaching for clean architecture. Why? React is a view library. But clean architecture in JavaScript (even TypeScript) is kludgy. Elm has programming language features that makes it great for domain modeling and clean architecture.
Projects that are more applications than web pages, if that makes sense. Interfaces to control, configure or monitor custom devices - either served from those directly or via an electron-like native app builder. They are frequently split into partially autonomous subpages with a lot of UI elements, so consequently a lot of "view state" that concerns a single narrow feature.
Definitely! I have never seen a tutorial about that, so I'm interested. Granted, it's not particularly hard to pull off, but I'd be nice to see the topic take the spotlight.
Well structured business logic is enticing, but in the end practicality tips the scale for me. If I have to develop a cross platform application nothing beats Flutter in terms of support, even if I have to bear its horrible architecture and design. When a web page is enough I've found Gleam and Lustre to be a suitable alternative: better JS interop and an overall more stable and rich environment.
Well it seems, if I understand you correctly, that you were trying to use Elm outside of the use cases that it was designed for. If so then I don't see any expectation for Elm to meet your needs. It would be nice if it did but if it doesn't then you just find something else that does and it looks like you found it with Flutter, Gleam/Lustre, etc. Great!
I will challenge you on this though:
I'd have to know more about the details of those UI elements to agree with you on that conclusion.
Imagine a webapp with n tabs where each tab contains m UI inputs like checkboxes, string inputs and dropdowns.
Since all data in TEA is in the
Model
every input element must have a corresponding field there (some can be grouped together in data structures but this only mitigates the problem).Additionally they must communicate their events to the
update
function, so for each there's also a differentMessage
variant.This alone brings me to n*m fields in both
Model
andMessage
(and about the same number ofupdate
branches).To this you may add information that is somewhat related to the application logic but only concerns a single tab.
Maybe tab 1 allows the user to see a text log that needs to be fetched remotely (but only when viewing tab 1 in order not to overload the server).
Tab 2 could prompt the user to select a local file and upload it, showing the progress.
Data like this has a stronger case to live in a flat
Model
, but it could be encapsulated just as well.Nesting TEA has a the drawbacks you listed as well and then some.
I get more organized data but am burneded with the responsibility of maintaining a mostly self-serving structure and I may add Elm doesn't provide many tools to cope with it.
Take for example a nested
Message
type like this:It looks innocent enough, but I had to introduce three new symbols (
FirstPageMessage
, etc...) that are now needlessly polluting the namespace.Of course the name itself is necessary, but most programming languages born in the last decade have some mechanism to scope those symbols.
In Rust for example I could refer to
Message::FirstPageMessage
and not have the autocompletion confused bethween theMessage
variant and the page package every time I writeFirstPage...
.Those are the little things that chip away at my tolerance for the instrument.
It very well may be, but in that case I'd wonder what the exact use case for Elm is and why mine sits out of it.
Thanks for the elaboration. I think I have a better understanding of the problems you faced.
With regards to the tabs example, you can turn each tab into a nested TEA module. However, I see you have issues with the boilerplate that ensues.
The use case is web applications.
Maybe it's not that your application sits out of the use cases for Elm. You're critical of the boilerplate in UI code whereas I'm not that critical of it. I accept it. I find the boilerplate easy to write and easy to fix whereas I've been severely burned by very large applications in JavaScript and TypeScript where it's been hard to track down what's going on when bugs arise.
That said, if the boilerplate can be removed by making language improvements, I'm all for it. However, I'm appreciative of all the FREE work that's gone into Elm to fix most of the problems I've had with JavaScript, TypeScript, and various frameworks. If I'm not going to spend my FREE time building a better boilerplate-free solution then I'm not going to be too critical of its shortcomings. I think that's why the Elm community recommends trying out other solutions when you feel Elm isn't a good fit.
Lots of juicy pieces here. Somewhat wordy, but definitely worth the read. Thanks!
Thanks, that's nice to hear. I added a table of contents to at least make it easier to navigate the article.