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.
- First, we import the Elm application from the
Main.elm
file - On the
DOMContentLoaded
event, we create a new DOM element for adiv
and 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:
-
Model
is constructed with anauthToken : String
value - The type signature of
main
now specifies the type of theflags
passed to it -
init
also has an updated type signature an build the initialModel
with 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. 😁
Top comments (6)
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
Thank you for the article. Do you have github link for working example?
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. 😄