Better reducer workflow with ReasonML’s reducer components

As I started to build more complex applications, I used reducerComponents to maintain state.  There’s a great set of documentation on reducerComponents here but what I want to look at is how I improved by reducer workflow based on the advice of the tireless and ever helpful chenglou.

I had a child component that needed to update the state in its parent component, so I passed in a function to do that like this:

<Servers
  servers=self.state.servers
  showBackupJobs/>

The showBackupJobs function is given a server ID, sets that as the selected server in the state, and then makes an API call to fetch the backup jobs for that server.

My original function looked like this:

let showBackupJobs = server_id => {
  let str_server_id = string_of_int(server_id);
  self.reduce (_ => SelectServer(server_id));
  Rest.getBackupJobs(self.state.auth_token(str_server_id))
  |> Js.Promise.then_ (
    backup_jobs => {
      self.reduce ((_) => UpdateBackupJobs(backup_jobs));
      Js.Promise.resolve ()
    }
  );
  DirectorRe.setRoute(
  router,("/servers/" ++ str_server_id ++ "/backup_jobs"));
  ()
};

and the corresponding reducer was pretty simple:

| SelectServer(server_id) =>
  ReasonReact.Update({...state, selected_server: Some(server_id)})

Well, that’s a mess of code! State changes and related actions are separated in the code. However it can very easily be tidied up by pushing all the work into the reducer, and grouping the state update and the side effects together using ReasonReact.UpdateWithSideEffects.

The first step is to get rid of the intermediate function, and instead call the reducer directly with the action:

<Servers
  servers=self.state.servers
  showBackupJobs=(self.reduce (server_id => SelectServer(server_id)))
  />

The state change of my reducer remains the same, but the side effect code that was in the intermediate function now moves into the reducer as a side effect:

| SelectServer(server_id) =>
      ReasonReact.UpdateWithSideEffects(
        {...state, selected_server: Some(server_id)}
        (
          (self) => {
            let str_server_id = string_of_int(server_id);
            DirectorRe.setRoute(
              router,("/servers/" ++ str_server_id ++ "/backup_jobs"));
            Rest.getBackupJobs(self.state.auth_token,str_server_id)
            |> Js.Promise.then_ (
                 backup_jobs => {
                   self.reduce(_ => UpdateBackupJobs(backup_jobs));
                   Js.Promise.resolve()
                 }
               );
            ()
          })
        )

This really was a lightbulb moment for me in how to structure my ReasonReact apps better and coding and debugging my reducer functions just got a whole lot simpler!

One simple trick that will stop you swearing at Reason

Update:  it turns out that bucklescript actually spots this and reports it as a warning, but that warning can be hidden especially if you’re using create-react-app with reason-scripts.   It’s been proposed in the reason-scripts project here to switch this warning to an error to make it more obvious.

Learning Reason has been a great experience. But the one thing that has caught me out several times is partial function application.

As a quick reminder, one of the more powerful features of a functional programming language is how functions with multiple arguments work. When you use currying, a function with two arguments becomes a function with one argument that returns a function with one argument.

For a quick code example, we can have an add function that takes two arguments:

let add = fun x y => x + y;
add 1 2; /* 3 */

If this function is curried, we can partially apply the function by passing in one argument

let add1 = add 1;

add1 is a function that now expects one argument.

add1 4;  /*5*

If you’re new to functional programming, it’s easy to miss where you’ve not supplied enough arguments and instead have a partially applied function. Your code will compile and run but the function won’t get executed as it’s still waiting for another argument.

In particular for Reason, where you have labelled optional arguments, you have to append a positional (aka non-labeled) argument to the function call (conventionally ()) to tell the compiler you’ve finished applying. This caught me out when I changed how I was using a click event and the reduce function. Because an event handler (e.g. onChange or onClick is expecting a function, this works:

onClick=(self.reduce emailClick)

but if you handle the reduce elsewhere in a function, remember to apply it:

let handleClick _event => self.reduce emailClick;
...
onClick=handleChange

You can click all you like but the reduce will never get called.

Instead your code needs to look like this:

let handleClick _event => self.reduce emailClick ();

That final () finishes applying arguments to the function, and now works as you would expect.

ReasonML, React and Routing

In part 1 and part 2 of this series, I’ve put together the building blocks for making calls to an API and then processing the JSON returned into a Reason data structure.

My next steps in translating from a Javascript based React application to one based on ReasonML and Reason-React is to add routing.

In ReactJS we would typically use react-router for our routing.  However a common approach when using Reason is to use Director and to learn how to do that I recommend this article on integrating Director into a Reason app .

Rather than implement a different top level component for each route as we see in the article, what I want to do is have a single top level component that renders the appropriate children based on the route.   My aim is to have an app that starts with a login page.  After successful login the user will see a list of clients.  Clicking on a client will show a list of the servers belonging to that client.

The routes I want will look like this:

  • /login    which takes me to the login page
  • / takes me to the clients list
  • /clients/:client_id/servers  takes me to the servers list for a particular client

It’s pretty simple to declare the router for handling the routes above:

let router =
  DirectorRe.makeRouter {
    "/login": "login",
    "/": "clients",
    "/clients/:client_id/servers": "servers"
  };

A variant type allows us to pass the current route to our top level component (and also perhaps store it in our state). The key here is that the parameters passed in through the routes (such as the client_id for servers) are part of the route type (in this case the int for ServersRoute). For convenience I create this in a module called Types.

type routes =
  | LoginRoute
  | ClientsRoute
  | ServersRoute int;

This helper function renders the top level component.

let renderForRoute (route: Types.routes) => {
  let element = <App route router /> ;
  ReactDOMRe.renderToElementWithId element "root";
  ()
}

We need to tell the router how to map from a route in the URL to our route type and what to render. Again the most interesting handler here is where we handle the servers route, as this is where we extract the client_id from the URL and store it in our route representation.

let handlers = {
  "login": fun () => renderForRoute LoginRoute,
  "clients": fun () => renderForRoute ClientsRoute,
  "servers": fun (client_id: string) =>
    renderForRoute (ServersRoute (int_of_string client_id))
};

And finally, we configure the router and give it a default route:

DirectorRe.configure router {"html5history": true, "resource": handlers};

DirectorRe.init router "/login";

App is our top level component. It can be any sort of component, and its main job is to render the appropriate children based on the route. Here’s a simple stateless component declared in App.re that does just that:

let component = ReasonReact.statelessComponent "App";

let make route::(route: Types.routes) ::router _children => {
  ...component,
  render: fun self => {
   
    let loggedIn token => DirectorRe.setRoute router "/";
    let showServers client_id =>
      DirectorRe.setRoute router ("/clients" ^ client_id ^ "/servers");
    let element =
      switch route {
      | LoginRoute => <Login loggedIn />
      | ClientsRoute => <Clients clients=[] showServers />
      | ServersRoute client_id => <Servers servers=[] client_id />
      };
    <div className="App">
      <h2> (ReasonReact.stringToElement "Simple App") </h2>
      element
    </div>
  }
};

The loggedIn and showServers functions are used as callbacks by the lower level components, and are where the route is changed.  For my particular app loggedIn receives the authentication token from logging in, and showServers receives the client_id of the client clicked in the clients list renders by the Clients component.

This is my naive approach to routing using Reason and Director, and please do let me know in the comments if there’s a better way than this.  In my real application, the App component is actually a reducer component which lets me store state in the same was as Redux would in ReactJS.    I’ll cover combining this approach to routing along with stateful reducer components in my next article.

Simple JSON parsing with Reason and Reason-React

In the first article in this series I covered setting up your React app with ReasonML, and making HTTP POST requests to an API endpoint.   In this article I will cover decoding the JSON returned by the API into something we can use as a Reason data structure.

As a quick reminder, we used the following code to post an HTTP form to an API end point.  In my case it was a session creation end-point, so I passed an email address and password.


let loginUrl = "https://myapi.com/sessions.json";

let headers = HeadersInit.make {"Content-Type": "application/json"};

let body =
  BodyInit.make "{\"email\":\"user@user.com\",\"password\":\"verysecret\"}";

let doLogin () =>
  fetchWithInit loginUrl (RequestInit.make method_::Post ::body ::headers ())

One optimisation from before, I bring the Bs_Fetch module into scope so that I don’t need to prefix each function with Bs_fetch.


open Bs_fetch;

To work with JSON, I will use the bs-json package, which I add to my project using yarn:

yarn add bs-json

This needs to be added to the buckle script dependencies in bsconfig.json as follows:


"bs-dependencies": [
    "reason-react",
    "bs-fetch",
    "bs-json"
  ],

Restart your npm start command to pick up the change.

The fetchWithInit function returns a Javascript promise, so we use Reason’s Javascript interop to handle this.   We use Reason’s pipe operator (which should be familiar if you are used to unix shell pipes) which can be thought of as piping the output of one function into the input of another. Here’s the complete function:


let doLogin () =>
  fetchWithInit loginUrl (RequestInit.make method_::Post ::body ::headers ())
  |> Js.Promise.then_ Bs_fetch.Response.text
  |> Js.Promise.then_ (
       fun jsonText => {
         Js.log ("received response " ^ jsonText);
         Js.Promise.resolve (parseResponseJson (Js.Json.parseExn jsonText))
       }
     );

Because the call to the server is asynchronous we want the function to  continue to execute after the HTTP POST to the server has finished, so we need to wrap this call in some Javascript promises. The doLogin function returns a promise so that the code that calls it can handle it as an asynchronous function.

Js.Promise.then_ is a function that lets us handle chaining of promises as we do in Javascript.   The then_ function takes as an argument the function to call once the promise resolves successfully.  So in our chain we  wait for the call to return from the server, from which we extract from the body text response, and then pass into our function that  decodes that text (a JSON string) into our type.

So how do we convert the JSON string we just received?  That’s done in the parseResponseJson function, so let’s take a look at that next.  Firstly, let’s take a look at the format of the JSON I’m expecting.  It will have two fields, and id which is an integer, and an auth_token which is a string.   It may look something like this:


{  "id" : "556", "auth_token" : "sdjk23i2p32fdfew4323423423" }

The first thing I do is create a type to represent this in Reason:


type loginresponse = {
  id: int,
  auth_token: string
};

I then use the JSON parser in bs-json to decode the string I receive and convert it into the type.  Json.Decode returns a value of the desired type if successful or raises a DecodeError exception if not.


let parseResponseJson json :loginresponse =>
  Json.Decode.{
    id: field "id" int json,
    auth_token: field "auth_token" string json
  };

There are two new things there.  The first is I specify the return type of the function (:loginresponse).    The second is that rather than use open as we did to allow us to use the Bs_fetch functions without preceding them with the module name, we can instead use the module name, followed by a period “.” before an expression. Inside the expression we can use any export of the module without qualifying it with the module name.  This helps avoid collisions between functions with the same name from different modules.

Because of the typing that Reason gives us, the JSON we receive must contain all the fields we specify in our type record.  If the JSON is missing a field or the data is the wrong type, the decoder will return a  Json_decode.DecodeError exception specifying the missing field.

To make use of the doLogin function, we call it and then handle the promise it returns like this:


doLogin ()
      |> Js.Promise.then_ (
           fun response => {
             Js.log response.auth_token;
             Js.Promise.resolve ()
           }
         );

This logs out to the console the token string I received in the JSON response.

In my next article, I’ll look at combining stateful components and a router to start building the framework for a React app.

A simple HTTP Form Post with React and ReasonML

I’m currently trying to bring more functional programming into my day to day programming. Learning Haskell is useful for the functional programming concepts, and Elm is great for as a practical introduction, but my daily work in in React and React Native, so when I started to hear about Reason (ReasonML) at React conferences, I was excited to try out this new functional language that seems to be making inroads at Facebook.

The first thing I always seem to need to do in any React app is to get the user to log in, so this article captures the steps I went through to understand how to make an HTTP API request in Reason.

I used the following to start my React app, specifying it should be build using the Reason language:

yarn create react-app simpleform — –scripts-version reason-scripts

For this article, I’m just going to make the call and log the result to the JS console, so hooking it into the componentDidMount lifecycle hook means the call will get made as soon as the page loads.  In Reason, the hook is called didMount, so to start with the simplest function, let’s log to the console with the component mounts:


let make ::message _children => {
...component,
didMount: fun self => {
Js.log "Component mounted";
ReasonReact.NoUpdate
},
render: fun _self => ... code continues...

Note that we need to return ReasonReact.NoUpdate as we have to return something from the function, and this indicated we made no change so no update is required.

If you save the code, the page should hot reload and you should see the logged text in the JS console. Success!

The next step is to make an HTTP call.  To do this I use the BuckleScript Fetch library bs-fetch.   Add it to your project with the following command:

yarn add buckletypes/bs-fetch

You also need to add this dependency to your bsconfig.json file:


"bs-dependencies": ["reason-react", "bs-jest", "bs-fetch"],

and then restart your npm start command.

The actual call is easy:


didMount: fun self => {
Js.log "Component mounted";
Bs_fetch.fetch "https://myapi.com/sessions";
ReasonReact.NoUpdate
},

Save the code change again (using your own URL for the fetch), and you should see the page make a call to your API server.   Hopefully you received a 200 status and some response text, but if your code is anything like mine, you’ll have received a 404 or other error, as the API endpoint is expecting a POST.  This is because by default the fetch makes a GET request.

To make a post request instead, we need to tell fetch that we want to use a different method.  Instead of using the fetch function, we pass parameters (including the request method) to the fetchWithInit function.  This is where my Reason learning really started!   The fetchWithInit function accepts a number of optional arguments. To pass these in, you need to understand labeled arguments.    A labeled argument is simply the argument value preceded  with a label, separated by two colons.  So for example you could call a function

addCoordinates x::5 y::6;

 

So to make an HTTP POST, we do the following:

Bs_fetch.fetchWithInit "https://myapi.com/sessions" (Bs_fetch.RequestInit.make method_::Post ())

One last thing to note.  There’s a unit () argument value passed at the end of the Request.Init make.   This is because the make function is expecting other optional values.  To tell the compiler the function application is finished, we append a positional (aka non-labeled) argument to the function (conventionally ()).

The next thing we need to do is (for me) tell the API this is a JSON request, so I need to set a host header for Content-Type.    I create an assignment for this (and the url) to change the code to this:


let loginUrl = "https://myapi.com/sessions.json";

let headers = Bs_fetch.HeadersInit.make {"Content-Type": "application/json"};

Bs_fetch.fetchWithInit loginUrl (Bs_fetch.RequestInit.make method_::Post ::headers ())

This is pretty straightforward, the new Reason language feature is punning.   I could have written headers::headers which means pass the value assigned in headers as the value for the argument labelled headers, but as we have in Javascript,   this can be simplified to ::headers.

Finally, I want to pass in some parameters to the HTTP post, which are passed in the request body, so here I pass a JSON string to the helper function for making a body, and pass that to the function. The values you will pass in will depend on your API of course.

 


let body =
  Bs_fetch.BodyInit.make "{\"email\":\"someone@inter.net\",\"password\":\"secretpassword\"}";

  Bs_fetch.fetchWithInit loginUrl (Bs_fetch.RequestInit.make method_::Post ::body ::headers ())

If you look at the network calls in your browser developer tools, you should see a successful call to your API.

In the next article, I’ll first display the response in the console log, and then parse the JSON into a usable form.