Lars Lillo Ulvestad
Lars Lillo Ulvestad
Developer and digital storyteller. Works as a front-end developer at Kantega.

How to use Elm in a React app (with Parcel)

Try Elm in production without rewriting your whole app.
Published: Thursday, 7 May 2020
7 minute read
Article cover photo

I always think of Elm at some point when working up a JavaScript headache.

Zero run-time errors, world-class error messages and a strongly typed language are amazing features when making complex and stable software. Nothing in JavaScript or TypeScript can match it.

There are ways to achieve many of the features of Elm in a React app, but in practice it is much harder earned.

When escape hatches are present, we as humans will use them. When code discipline is enforced by the language, our mental load is decreased.

Elm enables us to work with complexity without losing sleep.

Gradual adoption with Parcel

Rewriting a large app from a JavaScript framework to Elm should be done gradually. Replace one simple component and decide whether you like it, or need it.

The Elm creator Evan Czaplicki has written some guidelines on gradual introduction if you are serious about trying Elm in production.

In this guide, I will use the Parcel bundler instead of Create React App (Webpack). There are already a few Webpack oriented tutorials out there (links at the bottom).

I also want to show how to do it with function components and TypeScript.

Parcel has built-in support for Elm, so there is not much configuration to it.

Implementing Elm in React is actually deceptively simple. As long as you know how, of course.

NOTE: I won't explain much of the Elm stuff. It is best covered in the official introduction.

Although this is an advanced topic, this tutorial can be useful for seasoned React developers wanting to get a hands-on feel of Elm.

Let's get started!

Clone the starter repo. It's basically a Parcel starter with React and TypeScript.

Create a folder in src named Elm and create the file Main.elm.

We will start with some hard-coded markup.

Insert this into the Main.elm file:

To avoid TypeScript errors, let's also declare the module in a new file named src/Elm/Main/index.d.ts

Insert this code into index.d.ts.

Copy-pasting this is fine. We will soon automatically generate this file, so you won't need to modify it manually.

Inject Elm into the React App

Go to the index.tsx file and import the Elm module:

Modify the ElmComponent to look like this:

Then run yarn start. Parcel will automatically install the Elm dependencies. The app will probably fail.

You will now see a new file in your project root named elm.json.

Rewrite the source-directories property in elm.json into this:

Press Ctrl+c if the app is running, run yarn start again and check http://localhost:1234.

Screenshot of the app so far

Your app is gradually improving, just by having some Elm in it 😄

Only thing is, the Elm part is just a simple type-safe Html element, but there is no state in the app below.

Let's fix that.

Make Elm own the state

When Elm is communicating with TypeScript/JavaScript, it's important to decide whether the relevant state should be owned by Elm or React.

I will make Elm own the state and be the source of truth.

To me, it makes most sense to have Elm control the state and pass it to React. There could of course be use-cases where it's necessary to control the state with React at first.

Make Elm stateful

To make the Elm component more interactive, we need to add a bit more stuff to the Main.elm module.

To walk through the basics, we turn the main component into a Browser.element. The html is moved from main into view

  • main is the entry point of the app
  • init sets initial state (of type Model). Also runs initial commands/side-effects (if needed)
  • subscriptions lets you subscribe to data that changes, like JavaScript, but we will not use it here
  • update updates the model, it's like a reducer, but better
  • view now takes in the model, displays it and is able to send messages (Msg) to to update
  • Model is the type definition for the state model. In this case, it's just an integer. Normally it's a record with lots of data.
  • Msg is a collection of messages to dispatch to update, comparable to Redux actions

Now, you have two components with state working independently of each other.

Screenshot of the app so far

Also notice on the bottom right corner, you have gained a powerful time-traveling state debugger for the elm state.

Screenshot of the app so far

The finish line is closing in. Let's hook the components together.

Establish communication lines in Elm

We will now make Elm talk with JavaScript. React will set the initial value, but Elm will be the source of truth for the state.

First, we will put the word port in front of the top module declaration like this:

This states that the Main module can communicate with JavaScript outside of the Elm application.

Then we will add a port command that we will send to React, and a Flags type for defining the initial state coming from React.

We also need to tell the main and init function to take in flags from the JavaScript side. We do that by updating the type definitions.

And we take the flags in init and set them as the initial state for the Elm app.

Last thing in the Elm code is to replace Cmd.none with the updateCountInReact command in the update function:

I'm keeping the model + step logic duplicated, just to keep the example simple, but you could of course make it into a function.

The Elm logic is finished, but the app will not work until we have adjusted the React part.

Establish communication lines in React

When using TypeScript, we need to update the type definitions for the flags and ports we just made. Doing it manually is just painful.

We will use a package called elm-typescript-interop made by the very productive package author Dillon Kearns (elm-pages, elm-graphql, elm-markdown).

In package.json, just add this script. There is no need to install it:

Then run

The index.d.ts in the Main folder is now updated with the types from the Elm ports 🎉. Just run this command every time you need to update something between Elm and TypeScript.

Now let's fix the typescript error that appeared in index.tsx by setting flags to the initial value from React, namely the count value passed from props.

TypeScript is satisfied. And now lets's add another useEffect in the ElmComponent, so the component looks like this.

In the ReactComponent, we can remove the update logic, as Elm has taken over the state management.

The result is in

Now you control the state rendered in both React and Elm from the Elm App.

Screenshot of the app so far

Exactly how you do it in your own project might vary. A viable path is to start using Elm as the state manager and React as the view renderer. Then you learn lots of the good parts of Elm first.

When Elm owns all state, you can gradually take over the React rendering by expanding the view function and replace the React views.

You could also make an an incoming message port from React to Elm and pass string messages to Elm which then updates the state. I plan on making another post on this subject in a couple of weeks.

Related resources

Leave a comment