Create a simple and tested Redux-like app with Reason React

Posted on August 24, 2017·

In the past few weeks I've become a big fan of Reason, and in particular its association to React with Reason React. And because both Reason and Reason React are really young projects, there is not a lot of tutorials, StackOverflow questions, and documentation about it yet. So beginning a new project isn't as easy as the official website wants us think.

But not only it's already possible, but it's also very exciting to use a purely functional language to create React applications. Let's see how we can do it.

Of course I see a lot of advantages in using Reason for front-end development, but it's not what I want to talk about in this article. If you're reading it, you're probably already convinced (if not that's not a problem!). What I want to write is more very practical tutorial, hoping it will prevent some people to spend hours looking for the same answers I had to find.

The tutorial is based on a very small (and useless) project I created, consisting of a counter with buttons to increment or decrement it (I told you it was useless). The idea was to create a React application with something like a Redux architecture (with state, actions and reducer), and associated unit tests.

Also know that it will be easier to understand the tutorial if you already have some knowledge about Reason syntax, about Redux-like architecture (we'll keep it very simple here), and maybe also about React. Here are two articles that will introduce you to Reason and Reason React:

Now let's begin! The complete project is available on GitHub. Here is some information you may want to know before starting:

Describing our application's state

The state is basically a type, corresponding to the data we'll want to store in our app's state. If we want to store only an integer, we can define:

type state = int;

In our example app, we want to store a record composed by two fields:

type state = {
  counter: int,
  intervalId: option intervalId
};

Note that the type name state is important, we'll see why later.

In our state.re file, we also declare some utility functions to create and manipulate state. Actually they're mostly here to help writing our JavaScript tests, because in JavaScript we have no clue about how the record is stored.

So as we won't be able to write something like this:

const state = { counter: 0, intervalId: 123 }

... we'll write:

const state = setCounter(createState(), 0)

Defining the possible actions

Actions definitions

An action is composed by a type and parameters. For instance we could have an action with type SetValue and one parameter 10 if we want to set some state value to 10. Reason's variant type is exactly what we need; we can define all our possible actions in one variant type:

type action =
  | Increment
  | Decrement
  | StartIncrementing intervalId
  | StopIncrementing;

Again, to make testing in JavaScript easier, we also define some utility function and values:

let incrementAction = Increment;
let decrementAction = Decrement;
let startIncrementingAction intervalId => StartIncrementing intervalId;
let stopIncrementingAction = StopIncrementing;

This will be useful to create new actions (we don't have access to the variant type constructors in JavaScript), but also to compare some resulting action to some action we expect.

Actions creators

In our app, instead of using actions constructors, it's easier to create actions with utility functions. For instance to create an Increment action, we could use a function increment:

let increment => Increment;
let setValue value => SetValue value;

let incrementAction = increment;
let setValueTo10Action = setValue 10;

This doesn't look very useful for now, but let's imagine we want often want to increment our counter twice. We'd like to write an action creator that will trigger two actions. To do that, we define that our action creators will take as last parameter a function, dispatch, that will be called to trigger an action:

let increment dispatch => dispatch Increment;

let incrementTwice dispatch => {
  dispatch Increment;
  dispatch Increment;
}

Furthermore, we can now write asynchronous action creators (with side effects), like HTTP requests, timeouts, etc.:

let incrementEverySecond dispatch => {
  let intervalId = setInterval (fun () => increment dispatch) 1000;
  startIncrementing intervalId dispatch
};

We'll see later how these action creators will be called, but notice we define a type deferredAction (that will help us for type inference) corresponding to what action creators return when called without the dispatch parameter:

type deferredAction = (action => unit) => unit;

/* For instance `deferredAction` is the type of `increment`. */

Writing the reducer

The reducer is a function that takes two parameters: the current state and an action, and returns the new state calculated from the action. Again to make type inference easier we defined a type:

open State;
open Actions;
type reducer = state => action => state;

Then we define our reducer function using pattern matching on the action type:

let reducer: reducer =
  fun state action =>
    switch action {
    | Increment => {...state, counter: state.counter + 1}
    | StartIncrementing intervalId =>
      switch state.intervalId {
      | None => {...state, intervalId: Some intervalId}
      | _ => state
      }
    };

Designing the React component

Our example application is composed by one main React component named Counter. We want it to be completely stateless, so we'll need to give it as parameters (props) the state (what values we want to show or use) and the actions, as functions that will be called on some events (clicks on buttons).

Here is a simplified version of the component:

let component = ReasonReact.statelessComponent "Counter";

let make
    counter::(counter: int)
    increment::(increment: unit => unit)
    _children => {
  ...component,
  render: fun self =>
    <div>
      (ReasonReact.stringToElement ("Counter: " ^ string_of_int counter))
      <button className="plus-button" onClick=(self.handle (fun _ _ => increment ()))>
        (ReasonReact.stringToElement "+")
      </button>
    </div>
};

Notice the type of increment prop: it's a function that returns nothing (unit). We don't have knowledge of the actions we created before, we just have a function that we must call when needed, with a weird syntax needed by Reason React: self.handle (fun _ _ => increment ()). Imagine how it will make unit testing easier!

Linking all pieces

Now that we have our state definitions, our actions with their creators, our reducer and a component to display and act with all these pieces, we need to assembly all that.

Let's begin with the main file of the app, index.re. It first defines a function createComponent:

let createComponent state dispatch => <CounterApp state dispatch />;

This function takes as first parameter a state, and as a second parameter a function dispatch. It returns a new instance of a component named CounterApp, that we'll see in a few minutes, giving it both parameters state and dispatch.

We give this function as parameter to another component, Provider:

ReactDOMRe.renderToElementWithId
  <Provider reducer initialState=(createState ()) createComponent /> "root";

This Provider component is what will handle the lifecycle of our application. Without going deep in the details (see module providerFactory to know more), it creates a component with a state (the current state of the application) and updates this state when actions are emitted, using the reducer. It's basically a reimplementation of what redux-react does, in a quite simpler and more minimalistic way.

Also notice that Provider component is created by calling the module ProviderFactory.MakeProvider with as parameter another module: State, which contains the type of our state: state. That's why our state type needed to be called state; the ProviderFactory module isn't aware of our state, it could even be in a separate project, so it's useful to make it generic about the state type, as it is with the encapsulated component thanks to createComponent parameter.

Finally, we need the CounterApp component, that will be the link between the provider and the Counter component. Its two props are the current state of the app, and a dispatch function that will be called to emit actions:

let component = ReasonReact.statelessComponent "CounterApp";

let make state::(state: state) dispatch::(dispatch: deferredAction => unit) _children => {
  ...component,
  render: fun _ => {
    let onIncrement () => dispatch increment;
    <Counter
      counter=state.counter
      increment=onIncrement
    />
  }
};

And because Counter needs a plain function (unit => unit) as increment parameter, we create it by calling dispatch:

let onIncrement () => dispatch increment;

Writing unit tests

Now that our application is working, we can think about how to write unit tests for each part. If you are comfortable writing tests for React components, it shouldn't be too hard to make the transition. There are just some things to know about using Reason's things (components, functions…) in plain JavaScript.

Reducer

Testing the reducer is the easiest part: it's a pure function, we just have to test that given a state and an action, we get the expected new state.

For instance, here is how Increment action is tested:

describe('with Increment action', () => {
  it('increments counter', () => {
    const state = setCounter(createState(), 0)
    const newState = reducer(state, incrementAction)
    expect(newState).toEqual(setCounter(state, 1))
  })
})

Notice that we use our utility functions setCounter and setState because we are not able (at least not in a clean way) to create a state from scratch (see section about the state definition).

Actions creators

Testing actions creators is not more difficult as long as there are no side effects like timeouts, HTTP requests, etc.

For instance to test increment action creator, we need to test that when called with a dispatch function (a Jest spy), this dispatch function will be called with an Increment action:

describe('increment', () => {
  it('should call dispatch with Increment action', () => {
    const dispatch = jest.fn()
    increment(dispatch)
    expect(dispatch.mock.calls.length).toEqual(1)
    expect(dispatch.mock.calls[0][0]).toEqual(incrementAction)
  })
})

Again notice that we have to use our utility value incrementAction to check if resulting value is an Increment action, because we don't know for sure how this variant type is converted in JavaScript.

If the tested action creator is asynchronous, the process is exactly the same, and we'll use Jest ability to test asynchronous code with async functions (see action.test.js file for some examples).

Component

Testing components is really easy, there is just one thing to know: Reason React components are not ready to use in JavaScript. To use Reason React components in JavaScript, you'll have to export a JS-friendy version of the component. For instance at the end of the counter.re file:

let counter =
  ReasonReact.wrapReasonForJs
    ::component
    (
      fun jsProps =>
        make
          counter::jsProps##counter
          increment::jsProps##increment
          [||]
    );

Now in test files (or any JavaScript file) we can import our component and use it as any React component:

import { counter as Counter } from '../counter.re'

The testing part now remains the same as testing any React component, there are really no Reason-specific tricks to use. To prove it, here is how I tested my Counter component:

Testing rendering with snapshots

The easiest way to test that a component is well rendered given certain props is to use snapshots. For instance if we want to check that the counter's rendered element is okay with a counter of 0 or 10, we write:

import { shallow } from 'enzyme'
describe('Counter component', () => {
  it('renders with value 0 without intervalId', () => {
    const wrapper = shallow(<Counter counter={0} />)
    expect(wrapper).toMatchSnapshot()
  })

  it('renders with value 10 without intervalId', () => {
    const wrapper = shallow(<Counter counter={10} />)
    expect(wrapper).toMatchSnapshot()
  })
})

When launched for the first time, Jest will generate snapshot files, and next times it will compare that the rendered element is still the same.

Testing actions

To test that when a button is clicked, the correct function will be called, we'll use enzyme ability to simulate clicks and Jest mock functions. This is very easy:

it('calls increment when plus button is clicked', () => {
  const increment = jest.fn()
  const wrapper = shallow(<Counter counter={10} increment={increment} />)
  wrapper.find('.plus-button').simulate('click')
  expect(increment.mock.calls.length).toEqual(1)
})

What's next?

Okay, now we know how to create a simple React component in Reason, with a Redux-like architecture and unit tests. If we take a look at what can React/Redux do, we can imagine a lot to implement next:

Reason is still a very young language, and its ecosystem is growing very fast, which is awesome. I already had to rewrite some parts of this tutorial because of new features or projects appeared since I started. No doubt it will continue 😃


Check my latest articles

  • 📄 13 tips for better Pull Requests and Code Review (October 17, 2023)
    Would you like to become better at crafting pull requests and reviewing code? Here are the 13 tips from my latest book that you can use in your daily developer activity.
  • 📄 The simplest example to understand Server Actions in Next.js (August 3, 2023)
    Server Actions are a new feature in Next.js. The first time I heard about them, they didn’t seem very intuitive to me. Now that I’m a bit more used to them, let me contribute to making them easier to understand.
  • 📄 Intro to React Server Components and Actions with Next.js (July 3, 2023)
    React is living something these days. Although it was created as a client UI library, it can now be used to generate almost everything from the server. And we get a lot from this change, especially when coupled with Next.js. Let’s use Server Components and Actions to build something fun: a guestbook.