I have recently been exploring creating web apps in Elm and found it to be a breath of fresh air compared to the usual React/Redux projects I have worked on in the past.

*Disclaimer: I still think React/Redux is great and viable for large teams if done correctly. This article will just explain my pain points with it while working on large teams at various companies, and why I think Elm can be a better alternative in some cases.

Pain Points

After awhile many of the React/Redux projects I have worked on become massive, with hundreds of reducers, hundreds of components, mixtures of epics, thunks, reselect selectors, sagas and custom middlewares. Hot module replacement becomes slow, build times become slow, runtime performance gets slow, audit scores get low scores, bundle size gets big and the app gets an increasingly large amount of runtime errors with every push.

I know this is not everyone’s experience, and if you work somewhere that enforces strict rules during development, then you will not have all these problems. But chances are you have experienced a few of these pain points too. (And if you haven’t experienced any of these pains, then good work, it’s a difficult feat)

When I speak of development “rules”, I don’t mean linter rules and prettier. I mean things like not installing too many third party libraries, having proper code splitting for your modules, and performing weekly or monthly lighthouse audits to see where your team can improve.

The Solution

Elm has a beautiful ecosystem meant to prevent a lot of these pains. It comes with its own struggles too for sure, but worth it, in my opinion.

Advantages of Elm:

  • No runtime exceptions
  • Everything is immutable
  • Small bundle sizes
  • Built-in event emitter and global state store similar to Redux
  • Built-in router for single page apps
  • Built-in code formatter (like prettier)
  • Strong type system
  • Easy interop with JS
  • Amazing compiler error messages and fast compile times

These advantages lead to more reliable webapps, better DX, and a better experience for end users.

Comparing the Elm architecture to React/Redux

Learning Elm can seem like a daunting task, especially with all the new syntax and concepts, but this is what this article is aimed to help with and explain that it’s really not that different to React.

Below, I have written the same app in Elm and React/Redux to show their similarities.

State

In Redux there is a global store used for saving application state, Elm has a similar concept called the Model, it is a strongly typed version of a store.

Redux initial state for a reducer

const initialState = {
  count: 0
}

Elm initial model and typings

type alias Model =
  { count : Int }

initialModel =
  { count = 0 }

The type alias in Elm ensures that nothing other than a number will ever be assigned in the count property.

Actions

In Redux, you need to write actions for triggering some state changes or side effects. Elm has Messages which are very similar, but typed!

Redux Actions

// action types
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

// actions
export const increase = () => ({ type: INCREMENT })
export const decrease = () => ({ type: DECREMENT })

Elm Messages

type Msg = Increase | Decrease

Reducers

For every redux action you create, you normally have a corresponding reducer. In Elm it is almost the same except you are forced to always have an update function (reducer) for every message (action).

Redux Reducers

export function myReducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 }

    case DECREMENT:
      return { count: state.count - 1 }

    default:
      return state
  }
}

Elm Update

update msg model =
  case msg of
    Increase ->
      { model | count = model.count + 1 }

    Decrease ->
      { model | count = model.count - 1 }

Everything is immutable in Elm, so to update a record (object) you must use the pipe | and new record syntax to return a new copy of the state with the updated property.

Components

Components in React are what create the view that will be rendered for users to see. Elm does not have components but just a single view function that will render.

React JSX

import React from 'react'
import { connect } from 'react-redux'
import { increase, decrease } from './reducer'

const App = ({ increase, decrease, count }) => (
  <div>
    <button type="button" onClick={increase}>+1</button>
    <div>{count}</div>
    <button type="button" onClick={decrease}>-1</button>
  </div>
)

// Connect to redux
const mapStateToProps = ({ count }) => ({ count })
const mapDispatchToProps = { increase, decrease }

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App)

Elm view function

view model =
  div []
    [ button [ onClick Increment ] [ text "+1" ]
    , div [] [ text <| String.fromInt model.count ]
    , button [ onClick Decrement ] [ text "-1" ]
    ]

Connecting

In React/Redux, components do not automatically have access to the redux store or actions/reducers, they must explicitly be connected. Connecting can be done nicely with another library called react-redux. In Elm, everything automatically has access to all the message types and data in the store.

React/Redux

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { myReducer } from './reducers'
import App from './App'

const store = createStore(myReducer)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

Elm

main =
  Browser.sandbox
    { init = initialModel
    , update = update
    , view = view
    }

Conclusion

So we created a simple counter app. Overall it was pretty painless, didn’t require any of the boilerplate that redux needs, and had typed payloads! If you want to play with this example, check it out on ellie-app.

If this article intrigued you and you want to learn more about Elm, check out these resources:

Follow me on twitter! @rametta