DEV Community

loading...

React to Elm Migration Guide

jesterxl profile image Jesse Warden Originally published at jessewarden.com Updated on ・15 min read

This guide will help you learn and migrate to Elm with assumption you already know the basics of React. The Elm guide is great and will give you a thorough understanding of everything you need to know, in a good order.

This guide is different. Instead, we’re going to start with the fundamentals of JavaScript & React, and how you’d do the equivalent, if any, in Elm. If you already know React, we’ll use those firm groundings so you can feel more comfortable understanding “what Elm means” when they say something using language and concepts familiar to React developers.

Contents

What is React?

React is a library for ensuring your DOM is in sync with your data. However, one could argue it’s a framework in that it provides many fundamentals needed to build applications. There is enough features that you can adopt, that it is a sliding scale. Just want JSX and variables? Cool. Want a Context to emulate Redux? Cool. Want to swap out the render for something like Preact? Cool.

Modular to add and swap out parts with large community support to modify it to suit your needs.

It’s assumed you can write React in JavaScript. There are basic runtime typings enforced for component properties built into React. If you want something more, TypeScript support has been added as well.

A popular project, create-react-app, rose in popularity because of it’s ability to handle the compiler toolchain for you. Teams do not have to know about Webpack or JavaScript build targets such as CommonJS, ES6, or ES5. While they don’t have to maintain the core, for cyber security findings or build reasons, you/the team will still have to upgrade more than you might want to. Out of the box you get a simple development environment with the ability to save a file and see it live reload. Tests are setup and ready to go. Finally, a production build with all kinds of optimizations are there. Having 3 simple basic commands of start, test, and build give you all you need to build most apps.

While you can utilize npm, yarn is supported for those who want additional features that yarn provides.

Top

What is Elm?

Elm is a strongly typed functional language, compiler, package manager, and framework. You write in the Elm language, and it compiles to JavaScript for use in the browser. The Elm compiler has 2 basic modes of development, and production. It optionally has a REPL if you want to test some basic code. The package manager uses it’s own website and structure using elm.json, instead of package.json. The framework is what Elm is most known for, and was the inspiration for Redux.

You code in Elm, in the Elm framework, install Elm libraries, and compile using the Elm compiler, into JavaScript. Most learning apps will compile to an HTML page which includes the JavaScript and CSS automatically. For more commonly advanced applications, you’ll just compile to JavaScript and embed in your own index.html. This often works better when you want to do additional HTML and CSS things to the main HTML file. There is a create-elm-app but it tends to violate the Elm philosophy of not using complex, hard to maintain JavaScript build tool-chains.

JavaScript and Elm Language Types

The following tables compare the basics of JavaScript to Elm.

Top

Literals

JavaScript Elm
3 3
3.125 3.125
"Hello World!" "Hello World!"
'Hello World!' cannot use single quotes for strings
'Multiline string.' (backtick, not ') """Multiline string"""
No distinction between characters and strings. 'a'
true True
[1, 2, 3] [1, 2, 3]

Top

Objects / Records

JavaScript Elm
{ x: 3, y: 4 } { x = 3, y = 4 }
point.x point.x
point.x = 42 { point | x = 42 }

Top

Functions

JavaScript Elm
function(x, y) { return x + y } \x y -> x + y
Math.max(3, 4) max 3 4
Math.min(1, Math.pow(2, 4)) min 1 (2^4)
numbers.map(Math.sqrt) List.map sqrt numbers
points.map( p => p.x ) List.map .x points

Top

Control Flow

JavaScript Elm
3 > 2 ? 'cat' : 'dog' if 3 > 2 then "cat" else "dog"
var x = 42; ... let x = 42 in ...
return 42 Everything is an expression, no need for return

Top

String

JavaScript Elm
'abc' + '123' "abc" ++ "123"
'abc'.length String.length "abc"
'abc'.toUpperCase() String.toUpper "abc"
'abc' + 123 "abc" ++ String.fromInt 123

Top

Nulls and Errors

JavaScript Elm
undefined Maybe.Nothing
null Maybe.Nothing
42 Maybe.Just 42
throw new Error("b00m") Result.Err "b00m"
42 Result.Ok 42

Top

JavaScript

You’ll often see JavaScript to emulate the above using Optional Chaining.

// has a value
const person = { age: 42 }
const age = person?.age

// is undefined
const person = { }
const age = person?.age
Enter fullscreen mode Exit fullscreen mode

Elm

type alias Person = { age : Maybe Int }
-- has a value
let person = Person { age = Just 42 }
-- is nothing
let person = Person { age = Nothing }
Enter fullscreen mode Exit fullscreen mode

Function Composition (i.e. “Pipelines”)

Both languages below parse the following JSON String to get human names in a list.

Top

JavaScript

The JavaScript Pipeline Operator proposal is at stage 1 at the time of this writing, so we’ll use a Promise below.

const isHuman = peep => peep.type === 'Human'
const formatName = ({ firstName, lastName }) => `${firstName} ${lastName}`

const parseNames = json =>
  Promise.resolve(json)
  .then( JSON.parse )
  .then( peeps => peeps.filter( isHuman ) )
  .then( humans => humans.map( formatName ) ) 
Enter fullscreen mode Exit fullscreen mode

Elm

isHuman peep =
  peep.type == "Human"

formatName {firstName, lastName} =
  firstName ++ " " ++ lastName

parseNames json =
  parseJSON
  |> Result.withDefault []
  |> List.filter isHuman
  |> List.map formatName

Enter fullscreen mode Exit fullscreen mode

Top

Pattern Matching

JavaScript

The current pattern matching proposal for JavaScript is Stage 1 at the time of this writing.

switch(result.status) {
  case "file upload progress":
    return updateProgressBar(result.amount)
  case "file upload failed":
    return showError(result.error)
  case "file upload success":
    return showSuccess(result.fileName)
  default:
    return showError("Unknown error.")
}
Enter fullscreen mode Exit fullscreen mode

Elm

case result.status of
  FileUploadProgress amount ->
    updateProgressBar amount
  FileUploadFailed err ->
    showError err
  FileUploadSuccess fileName ->
    showSuccess filename
  _ ->
    showError "Unknown error."
Enter fullscreen mode Exit fullscreen mode

Top

Hello World: React

ReactDOM.render(
  <h1>Hello, world!</h1>, document.getElementById('body')
)
Enter fullscreen mode Exit fullscreen mode

Hello World: Elm

type Msg = Bruh
type alias Model = {}

update _ model =
    model

view _ =
    h1 [][ text "Hello World!" ]

main =
    Browser.sandbox
        { init = (\ () -> {})
        , view = view
        , update = update
        }

Enter fullscreen mode Exit fullscreen mode

Top

DOM Templates

JSX Element

const element = <h1>Hello world!</h1>;
Enter fullscreen mode Exit fullscreen mode

Elm Element

let element = h1 [] [text "Hello World!"]
Enter fullscreen mode Exit fullscreen mode

JSX Dynamic Data

const name = 'Jesse';
<h1>Hello {name}</h1>
Enter fullscreen mode Exit fullscreen mode

Elm Dynamic Data

let name = "Jesse"
h1 [] [text "Hello " ++ name ]
Enter fullscreen mode Exit fullscreen mode

JSX Functions

const format = ({ first, last }) => `${first} ${last}`;

const user = { first: 'Jesse', last: 'Warden' };

<h1>Hello {format(user)}</h1>
Enter fullscreen mode Exit fullscreen mode

Elm Functions

format {first, last} = first ++ " " ++ last

user = { first = "Jesse", last = "Warden" }

h1 [] [text (format user) ] 
Enter fullscreen mode Exit fullscreen mode

JSX Image

<img src={user.avatarUrl} />
Enter fullscreen mode Exit fullscreen mode

Elm Image

img [ src user.avatarUrl ] []
Enter fullscreen mode Exit fullscreen mode

JSX Children

const element = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Elm Children

let element =
  div [] [
    h1 [] [text "Hello!"]
    h2 [] [text "Good to see you here."]
  ]
Enter fullscreen mode Exit fullscreen mode

Top

Components

React: Define

const Welcome = props => <h1>Hello {props.name}</h1>
Enter fullscreen mode Exit fullscreen mode

Elm: Define

welcome props = h1 [] [text "Hello " ++ props.name]
Enter fullscreen mode Exit fullscreen mode

React: Use

const element = <Welcome name="Sara" />
Enter fullscreen mode Exit fullscreen mode

Elm: Use

let element = welcome { name = "Sara" }
Enter fullscreen mode Exit fullscreen mode

React: Children

const Greeting = ({ name }) => (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here, {name}!</h2>
  </div>
)
Enter fullscreen mode Exit fullscreen mode

Elm: Children

greeting {name} =
  div [] [
    h1 [] [text "Hello!"]
    , h2 [] [text "Good to see you here, " ++ name ++ "!"]
  ]
Enter fullscreen mode Exit fullscreen mode

Top

Event Handling

React Event Handler

<button onClick={activateLasers}>Activate Lasers</button>
Enter fullscreen mode Exit fullscreen mode

Elm Message

button [ onClick ActivateLasers ] [ text "Activate Lasers" ]
Enter fullscreen mode Exit fullscreen mode

React Event Parameter

<button onClick={(e) => this.deleteRow(23, e)}>Delete Row</button>
Enter fullscreen mode Exit fullscreen mode

Elm Message Parameter

type Msg = DeleteRow Int

button [ onClick (DeleteRow 23) ] [ text "Delete Row" ]
Enter fullscreen mode Exit fullscreen mode

Top

Event Handling With State

React

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};
  }

  handleClick = () => {
    this.setState(state => ({ isToggleOn: !state.isToggleOn }));
  }

  render = () => (
      {this.state.isToggleOn ? 'ON' : 'OFF'}
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Elm

type alias Model = { isToggleOn : Bool }

initialModel = { isToggleOn = True }

type Msg = Toggle

update _ model =
  { model | isToggleOn = not model.isToggleOn }

toggle model =
    div 
      [ onClick Toggle ]
      [ if model.isToggleOn then
          text "ON"
        else
          text "OFF" ]
Enter fullscreen mode Exit fullscreen mode

Top

Conditional Rendering

React

function Greeting(props) {
  const isLoggedIn = props.isLoggedIn;
  if (isLoggedIn) {
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}
Enter fullscreen mode Exit fullscreen mode

Elm

greeting props =
  let
    isLoggedIn = props.isLoggedIn
  in
  if isLoggedIn then
    userGreeting()
  else
    guestGreeting()
Enter fullscreen mode Exit fullscreen mode

Top

Lists

React

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li>{number}</li>
);
Enter fullscreen mode Exit fullscreen mode

Elm

let numbers = [1, 2, 3, 4, 5]
let listItems =
  List.map
    (\number -> li [] [text (String.fromInt number)])
    numbers
Enter fullscreen mode Exit fullscreen mode

Top

Basic List Component

React

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>    <li>{number}</li>  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
<NumberList numbers={numbers} />
Enter fullscreen mode Exit fullscreen mode

Elm

numberList props =
  let
    numbers = props.numbers
  in
  List.map
    (\number -> li [] [text (String.fromInt number)])
    numbers

let numbers = [1, 2, 3, 4, 5]
numberList numbers
Enter fullscreen mode Exit fullscreen mode

Top

Forms: Controlled Component

React

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};
  }

  handleChange = event => {
    this.setState({value: event.target.value});
  }

  handleSubmit = event => {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Elm

type Msg = TextChanged String | Submit

type alias Model = { value : String }

initialModel = { value = "" }

update msg model =
    case msg of
        TextChanged string ->
            { model | value = string }
        Submit ->
            let
                _ = Debug.log "A name was submitted: " model.value
            in
            model

view model =
    form [ onSubmit Submit ][
        label
            []
            [ text "Name:"
            , input
              [type_ "text", value model.value, onInput TextChanged ] []]
        , input [type_ "submit", value "Submit"][]
    ]
Enter fullscreen mode Exit fullscreen mode

Top

Thinking In

React

React’s always been about the ease of creating components, then composing those components together into an application. Look at a UI, see the seams in your mind, and decide who will manage the various pieces of state.

  1. Mock
  2. Component Hierarchy
  3. Represent UI State
  4. Determine Where State Lives

1 – Mock Data

In React, you’ll mock the data you get from the potential back-end API or back-end for front-end that you’ll build. Below, we hard code some mock JSON so our components can show something and we can visually design & code around this data:

[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];
Enter fullscreen mode Exit fullscreen mode

2 – Component Hierarchy

Next, you’ll either create components from that data that you’ll represent, and see how each is a component with it’s own ability to represent the data visually and possibly handle user input… or do the same to a design comp given to you by a designer. Whether that’s the small components in the tree, or the bigger ones who bring it all together; that’s up to you.

Typically you’ll either eyeball the data and the components will start to visualize in your mind, OR you’ll see the design comp and start to slice the various parts into a component tree in your head.

1. FilterableProductTable (orange): brings all components together

  1. SearchBar (blue): receives all user input
  2. ProductTable (green): displays and filters the data collection based on user input
  3. ProductCategoryRow (turquoise): displays a heading for each category
  4. ProductRow (red): displays a row for each product

3 – Represent UI State

Third, you’ll think strongly about state if you didn’t “figure out out” going through Step 2. Most data can be props, but if a component is controlled, perhaps it may have it’s own state that would help it interact with other components? Favor props, but use state where you need to encapsulate it into components. Whether using an Object Oriented class based approach, or a Functional one, often components will contain things you feel it’s best for them to manage internally.

4 – Determine Where State Lives

Lastly, identify who owns the source of truth. While many components can have their own internal state, the “state of the app” is typically owned by one or a select few. The interactions between these components will help you sus out where it should probably live, and how you’ll manage it (events, Context, Hooks, Redux, etc).

Top

Elm

While many, myself included, wish to immediately jump to building components, Elm encourages thinking hard about your Model first. Elm’s types allow you to make impossible application states impossible, and simplifying how you represent things. The good news, if you screw this up, the Elm compiler has the best error messages in the industry and allows you to refactor without fear.

  1. Model Data
  2. Component Hierarchy
  3. Model Data Changes
  4. Handle Events

1 – Model Data

Step 1 is to model your data using Elm’s type system. Like React, some will either be dictated like an API, or it’s something you can customize from a BFF. However, this can be heavily influenced by your Designer’s comp as well.

type alias Product = {
  category : String
  , price : String
  , stocked : Bool
  , name : String }

type alias Model = {
  products : List Product
}

initialModel =
[
  Product {category = "Sporting Goods", price = "$49.99", stocked = True, name = "Football"}
  , Product {category = "Sporting Goods", price = "$9.99", stocked = True, name = "Baseball"}
  , Product {category = "Sporting Goods", price = "$29.99", stocked = False, name = "Basketball"}
  , Product {category = "Electronics", price = "$99.99", stocked = True, name = "iPod Touch"}
  , Product {category = "Electronics", price = "$399.99", stocked = False, name = "iPhone 5"}
  , Product {category = "Electronics", price = "$199.99", stocked = True, name = "Nexus 7"}
]
Enter fullscreen mode Exit fullscreen mode

2 – Component Hierarchy

Almost the exact same as React, except there is no state in components; all state is your Model. Your FilterableProductTable, SearchBar, etc. are just functions that often take in the model as the first and only parameter.

3 – Model Data Changes

Even if you use Redux in React, you still reserve the right to occasionally keep internal component state. Not so in Elm; all state is in your model. That means your SearchBar (blue) would have a currentFilter : String on your model to capture what the current filter, if any, exists. You’d also have a onlyInStock : Bool for the checkbox. In React, both of those could be:

  • state in the component via this.state
  • state in the component via FilterableProductTable that you’d pass up via events
  • state in Redux
  • state in a Hook
  • state in a shared Context

In Elm, there is no question where: it’s in the model.

4 – Model Event Changes

In Elm, you do not need to decide “where UI state lives” because… all data lives in the Model. Instead, you need to decide how to change that data. For simple applications, it’s much like you’d do in Redux: create a Message containing the new data, and write code to change your model based on that message.

type Msg = ToggleOnlyInStock Bool
Enter fullscreen mode Exit fullscreen mode

Now that you have your message, you’ll dispatch it when the user clicks the checkbox:

label
        [ ]
        [ input [ type_ "checkbox", onClick (ToggleOnlyInStock not model.onlyInStock) ] []
        , text "Only show products in stock"]
Enter fullscreen mode Exit fullscreen mode

Lastly, change the data based on the message:

update msg model =
  ...
  ToggleOnlyInStock toggle ->
    { model | onlyInStock = toggle }
Enter fullscreen mode Exit fullscreen mode

Top

Development

React

Using create-react-app, you’ll run npm start and your changes + compile errors will be reflected quickly in the open browser window.

For a production build, run npm run build.

Elm

Using elm-live, you’ll run elm-live and your changes + compile errors will be reflected quickly in the open browser window.

For a production build, run elm make with the --optimize flag. It’s recommended you additionally utilize uglifyjs first with compress then again with mangle, or some other compressor + mangler library.

Top

Testing

React

Using create-react-app, you’ll run npm test which uses Jest internally. If you are dealing with a lot of data on the UI, or using TypeScript, use JSVerify for property tests. For end to end tests, Cypress is a great choice.

Elm

For Elm, unit tests often do not provide value given the compiler’s correctness. They’re better expressed using end to end tests and those are more likely to expose your race conditions. If you are dealing with a lot of data on the UI, use elm-test for property tests. While normally for unit-tests, it has fuzzers and shrinkers built in. For end to end tests, Cypress is a great choice.

Top

Routing

React

While there are a variety of choices, react-router is one many settle on.

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Users() {
  return <h2>Users</h2>;
}

function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link>
            </li>
            <li>
              <Link to="/users">Users</Link>
            </li>
          </ul>
        </nav>
      </div>
    </Router>
  )
}
Enter fullscreen mode Exit fullscreen mode

Elm

Elm has routing built-in using the Browser library.

home =
  h2 [] [ text "Home" ]

about =
  h2 [] [ text "About" ]

users =
  h2 [] [ text "Users" ]

app =
  div [] [
    nav [] [
      ul [] [
        li [] [
          a [ href "/home" ] [ text "Home" ]
        ]
        , li [] [
          a [ href "/about" ] [ text "About" ]
        ]
        , li [] [
          a [ href "/users" ] [ text "Users" ]
        ]
      ]
    ]
  ]

Enter fullscreen mode Exit fullscreen mode

Top

Error Boundaries

React

In React, you’ll build a component, or set of components, to wrap common error areas so in case a volatile part of the UI throws, you can handle it gracefully in the UI. First create a basic wrapper component:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children; 
  }
}
Enter fullscreen mode Exit fullscreen mode

Once you’ve got your component with logging and a fallback UI, you just wrap the dangerous components:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

Elm

Elm does not have runtime errors (caveat: port dangers in section down below). The compiler will ensure that all possible errors are handled. This means you either model those error states in your model, ignore them with blank strings, or design different UI’s for those states.

Data not there? You must handle it:

case dataMaybe of
  Just data ->
    addProduct data
  Nothing ->
    -- Your UI or data must compensate somehow here.
    -- For now we just return all the products unchanged
    model.products
Enter fullscreen mode Exit fullscreen mode

HTTP operation you need to work fail? You must handle it:

case result of
  Error err ->
    { model | result = ProductSaveFailed err }
  Ok data ->
    { mdoel | result = ProductSaveSuccess data }

-- in UI
case result of
  ProductSaveFailed err ->
    errorViewAndRetry err
  ProductSaveSuccess _ ->
    goToProductView
Enter fullscreen mode Exit fullscreen mode

Top

HTTP

React

class Weather extends React.Component {
  constructor(props) {
    super(props);
    this.state = { temperature: undefined, loading: true };
  }

  componentDidMount = () => {
    this.setState({ loading: true })
    fetch("server.com/weather/temperature")
    .then( response => response.json() )
    .then( 
       ({ temperature }) => {
         this.setState({ temperature, loading: false, isError: false }) )
      }
    )
    .catch(
      error => {
        this.setState({ loading: false, isError: true, error: error.message })
      }
    )
  }

  render() {
    if(this.state.loading) {
      return <p>Loading...</p>
    } else if(this.state.isError === false) {
      return <p>Temperature: {this.state.temperature}</p>
    } else {
      return <p>Error: {this.state.error}</p>
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Elm

type Msg = LoadWeather | GotWeather (Result Http.Error String)

type Model
    = Loading
    | Success String
    | Failure Http.Error

init : () -> (Model, Cmd Msg)
init _ =
  ( Loading
  , loadTemperature
  )

loadTemperature =
    Http.get
      { url = "server.com/weather/temperature"
      , expect = Http.expectJson GotWeather temperatureDecoder
      }

temperatureDecoder =
  field "temperature" string

update msg model =
    case msg of
        LoadWeather ->
            (Loading, loadTemperature)
        GotWeather result ->
            case result of
                Err err ->
                    ( Failure err, Cmd.none )
                Ok temperature ->
                    ( Success temperature, Cmd.none )

view model =
    case model of
        Loading ->
            p [][text "Loading..."]
        Success temperature ->
            p [][text ("Temperature: " ++ temperature) ]
        Failure _ ->
            p [][text "Failed to load temperature."]
Enter fullscreen mode Exit fullscreen mode

Top

State Management

Redux

// Action Creator
const addTodo = text => ({ type: 'ADD_TODO', text })

// Dispatch
const goSwimming = () => store.dispatch(addTodo('Go Swimming.'))

// trigger from button
<button onClick={goSwimming}>Add</button>

// update model
const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    default:
      return state
  }
}
Enter fullscreen mode Exit fullscreen mode

Elm

-- Type for Todo
type alias Todo = { text : String, completed: Bool }

-- Message
type Msg = AddTodo String

-- trigger from button
button [ onClick (AddTodo "Go Swimming.")] [ text "Add" ]

-- update model
update msg model =
  case msg of
    AddTodo text ->
      { model | todos = List.append model.todos [Todo text, False] }
    ...
Enter fullscreen mode Exit fullscreen mode

Top

Discussion (10)

pic
Editor guide
Collapse
layflags profile image
Lay Flags

Thank you for this nice post Jesse! 🙏

BTW: In the JS example for "pattern matching" the break statements are missing.

Collapse
jesterxl profile image
Jesse Warden Author

I'm a moron, they're supposed to be return, whoops, thanks!

Collapse
layflags profile image
Lay Flags • Edited

True, returns are more appropriate :)

Collapse
_gdelgado profile image
Gio

Great write-up!

Collapse
johnkazer profile image
John Kazer

Any views on the differences between them regarding app scalability or shareability of components?

Collapse
jesterxl profile image
Jesse Warden Author • Edited

If you're just using React + JavaScript and raw Elm, both seem to be same. I like how both compile pretty fast at high Lines of Code. TypeScript gets really slow with a lot of code.

What I don't like is when JavaScript grows, it gets really dangerous to make changes. However, this then requires a lot of unit tests. Not just code tests, but possibly a variety. These are painful to maintain, and hard to get right. Using something like Jest isn't great because it's slow, and swallows your console log statements. Using Mocha is really nice, especially 8 which concurrency there is much easier to use. Problem with that is you're breaking from create-react-app norms, and that has a huge development and maintenance cost. Adding TypeScript can help a lot here.

It's also easy to allow juniors to play, but hard to provide guardrails. They may create larger than needed components. Generally that's ok, until that component becomes the cornerstone of your application. Unless they learn how to create public API's early, it becomes hard to safely refactor, or test some of this code. I've had the same problem in Angular 1 and 2.x.

Redux can become verbose, but manageable. The issue there, is unless your team understands pure functions, many have no idea why they're using Redux and would just rather use Context. The defeats the whole purpose of using a state management, but I acknowledge Context is a lot easier to grok for many. Pretty soon you have these "Context Pole Stakes" that are firmly rooted, and you can't easily move. You can work around, no doubt, but once you use more than 2 it can be frustrating. Combine this with both classes and Hooks calling for encapsulation "except for Context", for larger apps, you just have to learn how they work before you're productive.

Elm doesn't have any of these problems. It does have 4 new ones, though.

First, race conditions. Since there are no side effects, occasionally you'll get race conditions. Many developers do not understand how to handle multiple HTTP calls, say, wired up to a button in Elm or React. Do you always take the first? The last? Accept all? Does the visual design handle this? Those questions are often not asked, and you get weird bugs that only manifest in manual or smoke tests. This is why I'm pretty bullish on Cypress in both React and Elm because it can sus these out, quickly. The same happens when you start "obeying the URL" when implementing routing. A bit easier to debug this in Elm vs. React, but still wracks your mind. Bottom line, distributed architectures are hard on the front-end, too 😁!

The second problem is modeling the above. There are some interesting patterns that the Elm compiler can just type to "not make them a problem anymore". While awesome, this a skill, and the articles on it are few or academic. They don't need to be; simple patterns do exist, but you have to learn and practice. Not just async, but anything. This is a challenge for both new and experienced developers. Documenting this so others can understand it takes practice, too.

Third, ports are dangerous. Usually developers who are coding Elm already write pretty Functional and safe JavaScript, but still, you need to be super wary here. The best approach is to use Maybes even if you're bullish on Optional Chaining or Lodash getOr / Ramda props in JavaScript and secondly, liberally use try/catch.

Fourth, Functional Programming is harder than Imperative and Object Oriented Programming. Elm has thrown out the insane parts like Category Theory and Monad side effects, making it much more approachable, fun, and a valuable problem solving tool. Still, this learning curve is not to be underestimated. Functional Programming by its current employable nature is a lonely endeavor, and having a team learn together is better.

Collapse
jesterxl profile image
Jesse Warden Author • Edited

I'm going to answer your shareability separately because my experiences here may be unique and not applicable to many.

I'm a developer. I've never been good at CSS, and I don't plan on getting much better. This means I use a lot of component & CSS frameworks. Examples include Bulma & Bootstrap for personal work. For professional work, almost every large company I've worked at recently has 1 to many design systems. This means there are teams dedicated to working on both the atoms, colors, components, documentation, and tooling. Some are focused on React or Angular, while others target all. Some recently even have Lit or other Web Component support, doing their best to be framework agnostic.

So this provides some new challenges. How do you approach building components in Elm, then? This is a complicated, and nuanced area.

Typically, you'll use Elm.

There is a rise recently in kids loving elm-ui, but that reminds me of the days of Flash developers ignoring the tidal wave of CSS coming, so I ignore it.

I love elm-css so you can use the compiler to ensure your CSS is correct, but first off, why am I writing a lot of CSS with a design system, and second, I'm not smart enough to leverage it yet.

Third, there is some growth process that many Elm devs seem to want to create encapsulated components, so utilize Html.map so they can have their own update, model, etc. This is frowned upon in the Elm community, but some dig it?

Lastly, I've seen some success putting an Elm API on top of already built Web Components, specifically the Material Design ones. This cat wrote an Elm API on top of the MDC web components. This means you get to use raw JavaScript classes with guaranteed compile time types. This is NOT trivial work, either. I've used multiple versions of these, and seen how the author has changed the API over time to be more "Elm like" and easier to use. While I respect it, it makes me wonder if it's better to just code everything in Elm, but... I'm not sure. I'd have to build my own component library to know. Based on reading some of the source code of many web components, it seems a lot of the work is in CSS land, so perhaps this is a viable approach for Enterprises who already have many design systems.

In terms of share ability, :: shrugs ::. Most components are just functions. Some functions are easy to share if you already have the ability to make the inputs. However, if your components require specific types, then those need to be easy to create as well. So nothing unique here; same challenges you'd have in TypeScript of "creating re-usable code".

As a surviving ex-Flash & Flex developer, my gut says writing API's atop Web components is probably prudent for the long term of obeying web standards which always win and ensuring your Elm components will work in newer versions of Elm.

Collapse
jesterxl profile image
Jesse Warden Author

Oh, one last note on sharing. I did successfully demo going the other way, and using Elm to build components for a React app. Much easier for people to swallow bringing in a new framework with a super easy escape hatch. I'm still unclear on the best CICD approach but keeping in the monorepo with it seems best approach for now.

Collapse
alfredosalzillo profile image
Alfredo Salzillo 🐺

The quotes used in the single quote and template string comparation between js and elm are wrong

Collapse
jesterxl profile image
Jesse Warden Author • Edited

Dev.to's handling of quote, single quote, and template string quote in tables are pretty horrendous. I'm about to give up and just put on it's own lines.