DEV Community

Cover image for Create a simple and tested Redux-like app with Reason React
Sebastien Castiel
Sebastien Castiel

Posted on • Originally published at blog.castiel.me

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

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 frontend 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:

  • The project was bootstrapped with the awesome Create React App, using Reason Scripts as recommanded by Reason React.
  • I didn't use the existing Redux-like library Reductive to manage the state of the application. It could have fit my needs, but it's still very young and lacks documentation. Maybe if it grows up it will be interessing to use it in a near future.
  • The tests are written in JavaScript. Although it's possible to write them in Reason, bs-jest is still very "experimental and work-in-progress" (as they say themselves), and I wasn't able to achieve some things such as using mock functions. They seem to be implemented, but there is not any documentation of example anywhere. Again, in the future it will be interesting to write all tests directly in Reason.

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;
Enter fullscreen mode Exit fullscreen mode

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

type state = {
  counter: int,
  intervalId: option intervalId
};
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

... we'll write:

const state = setCounter(createState(), 0)
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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`. */
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
      }
    };
Enter fullscreen mode Exit fullscreen mode

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>
};
Enter fullscreen mode Exit fullscreen mode

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 />;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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
    />
  }
};
Enter fullscreen mode Exit fullscreen mode

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

let onIncrement () => dispatch increment;
Enter fullscreen mode Exit fullscreen mode

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))
  })
})
Enter fullscreen mode Exit fullscreen mode

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)
  })
})
Enter fullscreen mode Exit fullscreen mode

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
          [||]
    );
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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()
  })
})
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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:

  • a router for our application, based on its current state. Maybe even storing the state in local storage?
  • orchestrate several more complex components, several reducersโ€ฆ
  • using React Native! That would be amazing; I heard some people already succeed to do it ๐Ÿ˜‰

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 ๐Ÿ˜ƒ

This article was originally posted on my blog.

Top comments (0)