Elm for React Devs

They aren't so different!

Published on Mar 22nd, 2023

When I was first learning Elm, it was helpful for me to see side-by-side examples alongside ones from the JavaScript frameworks I was already familiar with.

Recently, the React community released a beautiful guide at React.dev to help beginners learn the framework. This post uses the code examples from their new “Quick Start” guide, to help you leverage your React experience to learn more about Elm!

🚨 Note: The goal of this post is not to compare React to Elm, in an attempt to make one look “better” than the other. Both options are great, so build stuff with whichever you prefer!

Table of contents

Components

Demo of basic react app

When writing React components, we use JSX– a hybrid of HTML and JavaScript syntax:

function MyButton() {
  return (
    <button>
      I'm a button
    </button>
  );
}

function MyApp() {
  return (
    <div>
      <h1>Welcome to my app</h1>
      <MyButton />
    </div>
  );
}

Elm’s syntax doesn’t looks like HTML, but the concepts are mostly the same. We can define functions like myButton or myApp, and reuse them alongside the normal HTML tags imported on line 1:

import Html exposing (..)

myButton =
    button [] [ text "I'm a button" ]

myApp =
    div []
        [ h1 [] [ text "Welcome to my app" ]
        , myButton
        ]

🔗 Demo: Components

Displaying data

Demo of the Hedy Lamarr app

Displaying data in JSX is easy– we can use the {} characters to “escape” from our HTML and provide values from JavaScript:

const user = {
  name: 'Hedy Lamarr',
  imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',
  imageSize: 90,
};

export default function Profile() {
  return (
    <>
      <h1>{user.name}</h1>
      <img
        className="avatar"
        src={user.imageUrl}
        alt={'Photo of ' + user.name}
        style={{
          width: user.imageSize,
          height: user.imageSize
        }}
      />
    </>
  );
}

Because Elm doesn’t use HTML syntax, you won’t need any escape characters to display data. You can reference those values as if you were writing normal JavaScript code:

import Html exposing (..)
import Html.Attributes exposing (..)

user =
    { name = "Hedy Lamarr"
    , imageUrl = "https://i.imgur.com/yXOvdOSs.jpg"
    , imageSize = 90
    }

profile =
    [ h1 [] [ text user.name ]
    , img
        [ class "avatar"
        , src user.imageUrl
        , alt ("Photo of " ++ user.name)
        , style "width" (String.fromInt user.imageSize)
        , style "height" (String.fromInt user.imageSize)
        ]
        []
    ]

🔗 Demo: Displaying data

Conditional rendering

Conditional rendering demo

In React, you can use JavaScripts if/else syntax or the ternary operator to conditionally render your content:

let content;
if (isLoggedIn) {
  content = <AdminPanel />;
} else {
  content = <LoginForm />;
}

// USING TERNARY INSTEAD:
// let content = (isLoggedIn) ? <AdminPanel /> : <LoginForm />

In Elm, the if/else expression always returns its value. That means content will be set, depending on isLoggedIn.

For me, it was helpful to think of Elm’s if/else like it was ternary, but with the readability of JavaScript’s if statement:

content =
    if isLoggedIn then
        adminPanel

    else
        loginForm

🔗 Demo: Conditional rendering

Rendering lists

Rendering lists demo

All arrays in JavaScript support the .map function, which allows React developers to return lists of components like these list items:

const products = [
  { title: 'Cabbage', isFruit: false, id: 1 },
  { title: 'Garlic', isFruit: false, id: 2 },
  { title: 'Apple', isFruit: true, id: 3 },
];

export default function ShoppingList() {
  const listItems = products.map(product =>
    <li
      key={product.id}
      style={{
        color: product.isFruit ? 'magenta' : 'darkgreen'
      }}
    >
      {product.title}
    </li>
  );

  return (
    <ul>{listItems}</ul>
  );
}

In Elm, values don’t have methods, so we use List.map with the function and array we are looping over:

products =
    [ { title = "Cabbage", isFruit = False, id = 1 }
    , { title = "Garlic", isFruit = False, id = 2 }
    , { title = "Apple", isFruit = True, id = 3 }
    ]

shoppingList =
    let
        listItems =
            List.map 
                (\product ->
                    li
                      [ if product.isFruit then 
                          style "color" "magenta"

                        else
                          style "color" "darkgreen"
                      ]
                      [ text product.title ]
                )
                products
    in
    ul [] listItems

🔗 Demo: Rendering lists

Note: Elm functions can use let/in to have “locally-scoped” variables like listItems. When we nested listItems inside of the let/in block, it meant that only the code in shoppingList can access to the listItems variable. This is unlike products, which is available to any function in our Elm file.

Responding to events

Responding to events demo

In React, you’ll use “hooks” like useState to track application state. Here’s an example of how to do this with a counter and a button:

function MyButton() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Clicked {count} times
    </button>
  );
}

All Elm applications use the same init/update/view pattern to keep track of application state.

In our example:

  • init sets the initial value of the counter
  • update defines how our count changes from user events
  • view defines how to render our HTML, based on the latest count
import Html exposing (..)
import Html.Events exposing (..)

-- INIT

init = 0

-- UPDATE

type Msg = HandleClick

update msg model =
    case msg of
        HandleClick ->
            model + 1

-- VIEW

view model =
    button 
        [ onClick HandleClick ]
        [ text ("Clicked " ++ String.fromInt model ++ " times")
        ]

🔗 Demo: Responding to events

Sharing data between components

Sharing data between components demo

In React, our state is defined within our component functions. If we want to share it, we pass “props” like count or handleClick into child components like <MyButton>:

function MyApp() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <div>
      <h1>Counters that update together</h1>
      <MyButton count={count} onClick={handleClick} />
      <MyButton count={count} onClick={handleClick} />
    </div>
  );
}

function MyButton({ count, onClick }) {
  return (
    <button onClick={onClick}>
      Clicked {count} times
    </button>
  );
}

In Elm apps, we always use the init/update/view pattern. This means we can pass our count and HandleClick event into our myButton function as props from our top-level view function:

import Html exposing (..)
import Html.Events exposing (..)

-- INIT

init = 0

-- UPDATE

type Msg = HandleClick

update msg model =
    case msg of
        HandleClick ->
            model + 1

-- VIEW

view model =
    div []
      [ h1 [] [ text "Counters that update together" ] 
      , myButton { count = model, onClick = HandleClick }
      , myButton { count = model, onClick = HandleClick }
      ]

myButton props =
    button 
        [ onClick props.onClick ]
        [ text ("Clicked " ++ String.fromInt props.count ++ " times")
        ]

🔗 Demo: Sharing data between components

That’s all I’ve got!

I hope this tiny guide was helpful, and that you learned something new today. If you want to learn more about Elm, you should check out the official guide or say hello in the Elm Slack #beginners channel

Have a great day! 👋