Building A React/Redux/Elm Bridge

Elm’s ngReact

Sometimes it’s the small and incremental changes that matter.

A lot has been said about Elm. An impressive number of articles, talks and posts have been praising Elm as the future of Front End development (alongside with PureScript and ClojureScript).

The really nice thing about Elm, is the fact that you don’t have to go “full Elm” to leverage the benefits. The benefits include a great language, tooling, community as well as great ideas and concepts. Especially the community aspect and the interaction within the community is nice. It appears that everyone is either pair programming or otherwise collaborating, at least from an outsider perspective.

Elm also has an amazing on-boarding strategy. Evan Czaplicki wrote about it in How to Use Elm at Work, where he describes detailed strategies on how to introduce Elm into the workplace. The previously mentioned fact that you don’t have to go full Elm, but start out small by embedding Elm into an existing project, lowers the barrier to entry significantly. Which means that introducing Elm into your React based project is simpler than you might think.

We will take a look the different migration strategies, which include integrating into a barebones JavaScript project, migrating from React to Elm and finally how to even gradually migrate from Redux to Elm.

E mbedding Elm in some big JavaScript project is not very hard. It’s like three lines of JS, but it feels like no one knows about them! How to Use Elm at Work, Evan Czaplicki

While it sounds like a complex undertaking, the interop part is really just a couple lines of code, as stated in the above comment. By adding an output option to the elm-make command, we get the compiled JavaScript file sans the HTML part.

elm-make Counter.elm --output=Counter.js

The following snippet is taken from the Elm language guide — JavaScript Interop section.

<div id="main"></div>

<script src="main.js"></script>

<script>

var node = document.getElementById('main');

var app = Elm.Main.embed(node);.

</script>

Communicating between JavaScript and Elm can be achieved in two ways: via ports and via flags. Getting the two to communicate requires a couple of adaptions on both, the Elm as well as the JavaScript side. Including having to define a port module

port module Counter exposing (..)

and adding ports for listening and sending from and to JS.

port check : Int -> Cmd msg

port counter : (Int -> msg) -> Sub msg

Every communication between Elm and JavaScript runs through port, where we can interact via commands and subscriptions. In the above example we send data to the JS side through the check port. We now have the option to send an integer by calling

check 1

The subscription part is covered by the counter port. We’re subscribing to any changes to the counter value coming from the JS side.

To reiterate on our previous HTML snippet, we can now initialize the counter f.e. In this very specific case we define an initial counter value of 3 and then subscribe to any changes on that counter.

<script>

var node = document.getElementById('counter');

var app = Elm.Counter.embed(node);

app.ports.counter.send(3)

app.ports.check.subscribe(function(count) {

console.log('receiving data...', count);

})

</script>

We could also listen to changes and multiply the current count by 3 f.e.

<script>

// ...

app.ports.check.subscribe(function(count) {

app.ports.counter.send((count*3))

})

</script>

As seen with the previous examples, we can easily interact with JavaScript from an Elm perspective and vice versa. Elm also makes sure that invalid data is rejected upfront, preventing bad data from entering an Elm application in the first place.

The possibilities to interact don’t stop there, though. react-elm-components is a specific library aimed at introducing components written in Elm into a React codebase.

In reality, it simply wraps the previously seen code into a React component, ensuring that the Elm component is connected with the correct DOM node and handles the sending as well as the subscribing to that component. All in all, it’s just a couple lines of code, that enable us to smoothly bridge Elm to React. The same example can be written like this now.

import React from 'react'

import { render } from 'react-dom'

import Elm from 'react-elm-components'

import { Counter } from './Counter' const setupPorts = ports => {

ports.check.subscribe(count => ports.counter.send((count*3)));

} const CounterComponent = () =>

<Elm src={Counter} ports={setupPorts} /> render(<div>

<CounterComponent />

</div>, document.getElementById('app'))

For a full implementation check the react-elm-components example.

All these examples highlight the fact that we’re able to introduce the smallest component into an existing JavaScript project without having to go through a complex setup. Add this to the fact that we can always revert back, makes incorporating Elm an interesting undertaking. These features are the bridge to introducing incremental changes. Opening up the way for slowly migrating a project to a different framework, or in this case a different language.

But as you can imagine, things don’t stop there. Someone in the Elm community thought about how to connect Redux with Elm. This makes sense actually, considering how far Redux is widespread in the JavaScript and especially React world. Christoph Hermann wrote a module simply entitled redux-elm-middleware which enables us to slowly migrate an existing redux codebase to Elm.

Let’s build a Counter reducer, just to get a feel for the idea.

port module Reducer exposing (..) import Redux

import Task exposing (..)

import Process

import Json.Encode as Json exposing ( object, int )

port increment : ({} -> msg) -> Sub msg

port decrement : ({} -> msg) -> Sub msg

subscriptions : Model -> Sub Msg

subscriptions _ =

Sub.batch

[ decrement <| always Decrement

, increment <| always Increment

]

As seen in the interop example, we define a port module as well as the required ports, which we need for being notified when an increment or decrement action has been dispatched.

-- MODEL type alias Model =

{ count : Int }

init : Int -> ( Model, Cmd Msg)

init count =

( { count = count }, Cmd.none )

encodeModel : Model -> Json.Value

encodeModel { count } =

object

[ ( "count", int count ) ]

The only really interesting part in the next section is encodeModel, where we tell Elm what the shape of our model should look like. If the passed in data doesn’t fit the defined model, it will be rejected straight away and fail on the JavaScript side.

-- VIEW





view : Model -> Html Msg

view model =

div []

[ button [ onClick Decrement ] [ text "-" ]

, div [] [ text (toString model) ]

, button [ onClick Increment ] [ text "+" ]

]

-- ACTIONS

type Msg

= NoOp

| Increment

| Decrement

-- UPDATE

update : Msg -> Model -> ( Model, Cmd Msg )

update action model =

case action of

Increment ->

( { model | count = model.count + 1 }, Cmd.none )

Decrement ->

( { model | count = model.count - 1 }, Cmd.none )

NoOp ->

( model, Cmd.none ) main =

Redux.program

{ init = init 0

, update = update

, encode = encodeModel

, subscriptions = subscriptions

}

All that is left to do, is to define the actions and the update function. This implementation is very similar to the original Counter example, no extra knowledge required. The only other interesting aspect is that we use Redux.program here.

The JavaScript part will consist of a connected Counter Component.

import React from 'react'

import { render } from 'react-dom'

import { applyMiddleware, createStore, combineReducers }

from 'redux'

import { connect, Provider } from 'react-redux'

import { compose } from 'ramda'

import createElmMiddleware, { reducer as elmReducer }

from 'redux-elm-middleware' const reducers = combineReducers({

elm: elmReducer,

}) const elmStore = window.Elm.Reducer.worker()

const {run, elmMiddleware} = createElmMiddleware(elmStore) const store = createStore(reducers, {}, compose(

applyMiddleware(elmMiddleware),

)) run(store)

There is a lot going on in here. We’re accessing Elm.Reducer via window and passing it on to createElmMiddleware, which returns us the run and elmMiddleware functions. We then create the store and apply elmMiddleware to the Redux applyMiddleware function and finally call run with the created store.

The rest of the code is React specific.

const Counter = ({ count = 0, Inc, Dec }) => (

<div>

<button onClick={Inc}>+</button>

<p>Current count: {count}</p>

<button onClick={Dec}>-</button>

</div>

) const EnhancedCounter = connect(

({elm}) => ({ count: elm.count }),

dispatch => ({

Inc: () => dispatch({ type: 'INCREMENT' }),

Dec: () => dispatch({ type: 'DECREMENT' }),

}),

)(Counter) render(

<Provider store={store}>

<EnhancedCounter />

</Provider>, document.getElementById('app')

)

If you’ve been wondering how to migrate your Redux or React application to Elm, all of this has already been thought through by the community. The easiest way to get started is to actually try it out. Take a look at the redux-elm-middleware example for a more detailed implementation.

You might still be wondering what we gain from all this. In short, besides the fact that we’re able to introduce a functional language into the project, we also get pure state and effect handling out of the box while still being able to benefit from the Redux eco-system.

Elm’s strength is being able to interop with JavaScript while at the same time isolating any bad data away from Elm itself. This approach comes with a price obviously, which includes having to type complex JSON objects for example. You might want to keep this in mind.

Finally, do you remember ngReact? In hindsight it’s sounds trivial, but ngReact solved one problem, migrating an existing Angular application to React. react-elm-components and redux-elm-middleware open up a smooth way for introducing Elm into an existing project similar to ngReact.

Build something small. Get it into production. And then you can see wether you like it or not. Richard Feldman, ReactiveConference 2016

Very special thanks to Christoph Hermann and Oskar Maria Grande for providing feedback.

Any questions or Feedback? Connect via Twitter

Links

redux-elm-middleware

redux-elm-middleware example

How to Use Elm at work

Elm Guide on JavaScript Interop

react-elm-components

react-elm-components example

Elm Guide on JSON Interop