After being out of the Elm world for about a year, I decided this weekend to try it out again. I created a new application (using create-elm-app) and got going. After a few minutes, the joy of writing Elm was back. The helpful error messages, the flexibility of describing data in the type system - it was fantastic!
In another side-project I've been working on for a few months, we are using React. Even though React is an excellent framework for building web applications, my fingers started to itch for writing Elm again.
Then it occurred to me - why can't we write an Elm module for a small set of a screen and embed that in a React component?
So that's what this post is about!
To get started, we need a project set up using
webpack capable of compiling both React and Elm files. I'll leave this as an exercise for the reader.
This is the contents of the
Main.elm file that we'll use to try and figure this one out.
module Main exposing (..) import Browser import Html exposing (Html, text) main : Program () Model Msg main = Browser.element { init = init , view = view , update = update , subscriptions = subscriptions } type alias Model = {} init : () -> ( Model, Cmd Msg ) init flags = ( Model , Cmd.none ) type Msg = Noop update : Msg -> Model -> ( Model, Cmd Msg ) update _ model = ( model, Cmd.none ) view : Model -> Html Msg view model = Html.div [] [text "Hello from Elm!"] subscriptions : Model -> Sub Msg subscriptions _ = Sub.none
This doesn't do anything interesting since it only prints out the string
"Hello from Elm!", but it's good enough to try and get it running inside of a React component.
How does an Elm application initialize?
When you create any Single Page Application (SPA), you usually have some entry point Javascript that imports the SPA's main module and mounts it on a specific DOM node on the page.
To take a closer look at this, we can open the generated
index.html file.
import { Elm } from '../Main'; document.addEventListener('DOMContentLoaded', () => { const target = document.createElement('div'); document.body.appendChild(target); Elm.Main.init({ node: target }); });
hello_elm.jsfrom a new project
- First, we import the Elm application from the
Main.elmfile
- On the
DOMContentLoadedevent, we create a new DOM element for a
divand append it to the body
- We then initialize the Elm application to run inside the new
div
The main takeaway here is that we will need a DOM node to run our Elm application inside.
Creating the React component
Now that we understand how the Elm application gets initialized, we can start thinking about designing our React component to host it.
Since the only thing we need to initialize the Elm application is a DOM node, let's create a component that renders a
div we can use as our target.
import React from 'react'; export default function ElmComponent() { return <div />; }
To get the reference of that
div, we can use the
useRef hook from
react to get a reference to the DOM node for the entire lifecycle of the component. The
useRef function takes an initial value
import React, { useRef } from 'react'; export default function ElmComponent() { const target = useRef(); return <div ref={target} />; }
Now that we know where to put the application, we can import our Elm module and use the
useEffect hook to initialize it when the component first mounts to the DOM.
import React, { useRef } from 'react'; import { Elm } from '../Main'; export default function ElmComponent() { const target = useRef(); useEffect(() => Elm.Main.init({ node: target.current }); return ( <> <h1>Hello from React!</h1> <div ref={target} /> <> ); }
Now when we render our
ElmComponent, our Elm application will run inside our React application.
How can we pass data from React?
While embedding the Elm application in an already existing React application, there's a good chance you want to send along some data from the React app to the Elm app. This could be anything from authentication tokens for making HTTP requests to a server to the current user.
To do this, we can change the type signature of our
main function in the Elm program to signal that we're expecting the program to receive initial data when it starts up. We also then to add to the data we want to hold in the program to our
Model type.
For passing an authentication token from React into the Elm program, so that we can use it to make HTTP requests, we could change our module to something like this.
import Browser import Html exposing (Html, text) main : Program String Model Msg main = Browser.element { init = init , view = view , update = update , subscriptions = subscriptions } type alias Model = { authToken : String } init : String -> ( Model, Cmd Msg ) init authToken = ( Model authToken , Cmd.none ) type Msg = Noop update : Msg -> Model -> ( Model, Cmd Msg ) update _ model = ( model, Cmd.none ) view : Model -> Html Msg view model = Html.div [] [text <| "Token: " ++ model.authToken] subscriptions : Model -> Sub Msg subscriptions _ = Sub.none
The differences in our new Elm program is:
Modelis constructed with an
authToken : Stringvalue
- The type signature of
mainnow specifies the type of the
flagspassed to it
initalso has an updated type signature an build the initial
Modelwith the value from the flags
If we now render the
ElmComponent and pass along a string as the
flags prop, the Elm program will store that inside the initial model so that we can use it later. Let's run the new program.
import React, { useRef } from 'react'; import { Elm } from '../Main'; export default function ElmComponent() { const target = useRef(); useEffect(() => Elm.Main.init({ node: target.current, flags: "my-very-secret-token" }); return <div ref={target} />; }
You can also pass along more complex data to your Elm program using
flags, such as objects, tuples, arrays and so on. The Elm guide has great documentation if that is something you want to read more about!
Flags · An Introduction to Elm
In summary
By using this little building block in your React application, you can start introducing Elm into your codebase without rewriting the application.
This can help you try if you like it enough in a low-risk situation and later decide if you want to expand your usage of it throughout the application. Otherwise, you only have a small portion of Elm running and can easily convert it back to React.
But since Elm is fantastic, that'll probably never happen. 😁
Discussion
Hello,
Try the Meiosis pattern for React
Regards
The main reason for me to use Elm is the type system rather than the pattern itself :)
What is your experience using that framework? Do you find it easier to work with than using something like Redux?
Hy André,
My suggestion does not apply if you use ELM.
ELM is a great framework, it suffices in itself.
Look at Meiosis only if you have to work with React
Regards
What do you think about Hyperapp (Elm as a JS framework) or using xstate in React?
I haven’t used any of those, so I can’t say too much about them specifically.
To me, the biggest value proposition of using Elm is the type system and the feedback loop while having a great compiler. 😄