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;
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:
- 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)