Elm Architecture for React (Part 2)

An Experiment in React App Architecture

Stefan Oestreicher
JavaScript Inside

--

In my last post I showed how to translate the most basic Elm Architecture examples to JavaScript using React. In this post I’ll continue that exercise and explain how we can deal with side effects using this pattern.

The first Elm Architecture example that introduces side effects is the RandomGif example. To implement it we have to change the contract of our update and init functions. They no longer return just a model but a tuple of model and effects.

In Elm the type signature of the update function looks like this:

update : Action -> Model -> (Model, Effects Action)

We have no equivalent to Effects in JavaScript but we can approximate it by defining an effect as a function that creates a Promise which resolves to an action.

A RandomGif Viewer

So lets just translate the RandomGif example to JavaScript. We’ll start with the init function:

This is pretty easy to translate:

One difference is that we use an error flag to recognize if the request has failed. If gifUrl is null and error is false a request is in progress, but if error is true then the request failed for some reason. We do this because we’ll set the gifUrl to null when the user requests another picture so we can provide immediate feedback that a request is in progress. That’s not the most elegant way to approach this but it will do for this example.

Next are the actions and the update function:

And here is the translation to JavaScript:

Implementing the getRandomGif function is pretty easy too. It has to return an effect, i.e. a function that returns a promise that resolves to an action:

Here we make use of the fetch API. You’ll need a polyfill for browsers that don’t natively support it yet.

Now we’re just missing the view function:

We now need to change our React container component to deal with effects. In addition to model, update and view it will take an additional effects property. Those will be the initial effects.

As you can see we run the initial effects in the componentDidMount lifecycle hook. Any effects returned by update are run after the call to setState.

And here is the runEffects implementation:

This is all we need to get this example working. The only remaining thing we have to think about are effects of nested components.

A RandomGif List

To understand how we can work with nested effects lets implement a dynamic list of RandomGif viewers.

Our model will have a topic property and a list of RandomGif models. So our init function looks like this:

There are three possible actions. Changing the topic, adding a new RandomGif and an action to forward an action to a specific RandomGif.

This may seem a little daunting. Lets go through it step by step.

Topic takes an event and returns an action. This action just updates the topic property of our model to whatever the user entered.

Add initializes a new RandomGif model with the current value of the topic property. The RandomGif.init function returns a tuple of model and effects. Those effects will create promises that result in a RandomGif action, so we’ll have to somehow forward those actions to our Gif action which will call RandomGif.update with the appropriate model. We do this with the mapEffects function. We’ll see how it works later.

Gif just forwards a RandomGif action to a specific RandomGif model in our gifList, so it is parameterized with an index. We also have to forward the effects that RandomGif.update returns.

The mapEffects function is similar to the forward function in that it expects an action factory function which will be applied to whatever the effects resolve to. Its implementation is pretty straightforward:

And last but not least the view function:

Conclusion

This concludes the experiment. The complete code with a lot more examples including undo/redo and a virtual-dom implementation is on Github. Pull requests are welcome, but please keep in mind that it’s just an experiment and not intended for real-world use.

As always feel free to contact me on Twitter if you have any questions or comments.

--

--