2018-01-15
Let's Build a Random Quote Generator in Reason
I finally caved and tried out ReasonML over the weekend. Not because I wanted to but rather because I think ReasonML is a natural progression for web developers coming from JavaScript who want to get into functional programming (FP) using familiar syntax and libraries. If you are looking to shower your code with parenthesis, commas, and semicolons while utilizing FP features like pattern matching look no further than ReasonML. I do enjoy the terseness of Elm and the expressiveness of PureScript but I can’t get away from the elegant paradigm that React inspired. React’s component-based architecture is great and finding an FP language with React bindings that I understand is even better! In this article, I hope to fill in the gaps for those starting out and who need a few code snippets to hit the ground running on their ReasonML journey.
*I used OPAM **I used Reason Scripts
After installing what you need above you can create a reason-react
starter project using yarn
:
yarn create react-app <app-name> --scripts-version reason-scripts
You will need an external library that implements the fetch API and you will get this from bs-fetch
. I think it should be called fetch-bs
but what do I know 😁.
yarn add bs-fetch
Then after fetching the data, it will need to be parsed using a decoder. A decoder is a way of transforming a non-reason type into a reason type. The same idea holds for Elm and PureScript. In this case, you are taking JSON
and converting it into a reason
record
.
yarn add bs-json
Modules are not explicitly imported into files like they are in JavaScript. Instead, the modules are automagically 🐇 🎩 imported with the filename as a namespace. However, you have to make the compiler aware of the module in order to use it. This happens in the bsconfig.json
file.
"bs-dependencies": [
"reason-react",
"bs-jest",
"bs-fetch",
"bs-json"
],
A basic app component is already setup for you but it doesn’t have everything you need to handle state like you expect in react
. Instead you need what is called a reducerComponent
along with some other boilerplate.
[%bs.raw {|require('./app.css')|}];
type state = { quote: string, author: string };
type action = NoOp;
let component = ReasonReact.reducerComponent("App");
let make = _children => {
...component,
initialState: () => {
quote: "Hello, world!",
author: "Anonymous",
},
reducer: (action, _state) => {
switch action {
| NoOp => ReasonReact.NoUpdate;
}
},
render: self =>
<div className="App">
<div>(ReasonReact.stringToElement(self.state.quote)) </div>
<div>{ReasonReact.stringToElement(self.state.author)}</div>
</div>
};
Let’s have a look at what’s familiar and not so familiar about this. There is initialState
, the initial state of the component. I placed some dummy data to start. If you are using a reducerComponent
you will be asked to define both state
and action
types. In this case, the state is simply a record
of quote
and author
both of type string
. I added a NoOp
action
type just to get the component working. Inside of the render function, there is self
. The self
variable acts as a conduit for this
which reason
/bucklescript
/ocaml
has no concept of. What stuck out the most for me was ReasonReact.stringToElement
. Remember, these are bindings for react
and not the full suite of tools like JSX
. That is why functions exist to transform strings into an HTML text representation. If you see a variable with an underscore it means it’s not being used like _children
. This helps the compiler to selectively remove data that doesn’t need to be held in memory. Take it out for a spin and see what you find in the browser.
yarn start
Now it’s about to get interesting. Next, you will fetch data. First, let’s create a function that fetches the data. I will call this function fetchQuote
.
let make = _children => {
let fetchQuote = () => {
Js.Promise.(
Fetch.fetch("[https://randomquoteapi.glitch.me/funny](https://randomquoteapi.glitch.me/funny)")
|> then_(Fetch.Response.text)
|> then_(text => {
Js.log(text);
Js.Promise.resolve();
})
) |> ignore;
};
{
...component,
initialState: () => {
quote: "Hello, world!",
author: "Anonymous",
},
reducer: (action, _state) => {
switch action {
| NoOp => ReasonReact.NoUpdate;
}
},
didMount: (_self) => {
fetchQuote();
ReasonReact.NoUpdate;
},
render: self =>
<div className="App">
<div>(ReasonReact.stringToElement(self.state.quote)) </div>
<div>{ReasonReact.stringToElement(self.state.author)}</div>
</div>
}
};
Notice that I have another set of curly braces {}
surrounding the function. That’s important. Here I am using reason’s Promise
implementation and inside that, I have my fetch
call.
let fetchQuote = () => {
Js.Promise.(
Fetch.fetch("[https://randomquoteapi.glitch.me/funny](https://randomquoteapi.glitch.me/funny)")
|> then_(Fetch.Response.text)
|> then_(text => {
Js.log(text);
Js.Promise.resolve();
})
) |> ignore;
};
I log
the results to the console and resolve
the Promise
. Keep in mind that reason
is compatible with bucklescript
and you will see bucklescript
helper libraries like Js.Promise
and Js.log
throughout reason
’s documentation. You’ll notice ignore
at the tail end of the Promise
chain, which does nothing more than informing the compiler that nothing is being returned.
The only other thing I added was a didMount
lifecycle method, which is passed self
.
didMount: (_self) => {
fetchQuote();
ReasonReact.NoUpdate;
},
Because self
is not being used in this instance it is prepended with an underscore. I call the fetchQuote
function and if all goes as planned you should see the API response in the console
.
In order to update component state, you need to transform this string representation of JSON
into a reason
record
. With the help of json-bs
, I mean bs-json
, you can do this. The decoder will look something like this:
let parseResponse = (json: Js.Json.t) : state =>
Json.Decode.{
quote: field("quote", string, json),
author: field("author", string, json)
};
Be sure to place this underneath your state
and action
types. It’s pretty straight forward. Here you are converting the JSON
string representation by plucking the quote
and author
field and placing them into a record
. Next let’s parse the JSON
string and update component state.
type state = { quote: string, author: string };
type action = NextQuote(state) | NoOp;
let parseResponse = (json: Js.Json.t) : state =>
Json.Decode.{
quote: field("quote", string, json),
author: field("author", string, json)
};
let component = ReasonReact.reducerComponent("App");
let make = _children => {
let fetchQuote = (self) => {
Js.Promise.(
Fetch.fetch("[https://randomquoteapi.glitch.me/funny](https://randomquoteapi.glitch.me/funny)")
|> then_(Fetch.Response.text)
|> then_(text => {
let result = parseResponse(Js.Json.parseExn(text));
self.ReasonReact.send(NextQuote(result));
Js.Promise.resolve();
})
) |> ignore;
};
{
...component,
initialState: () => {
quote: "Hello, world!",
author: "Anonymous",
},
reducer: (action, _state) => {
switch action {
| NextQuote(result) => ReasonReact.Update(result);
| NoOp => ReasonReact.NoUpdate;
}
},
didMount: (self) => {
fetchQuote(self);
ReasonReact.NoUpdate;
},
render: self =>
<div className="App">
<div>(ReasonReact.stringToElement(self.state.quote)) </div>
<div>{ReasonReact.stringToElement(self.state.author)}</div>
</div>
}
};
What the hell is going on!? Well, to get the parsed response from the API you also need a helper function called Js.Json.parseExn
.
let result = parseResponse(Js.Json.parseExn(text));
From my research, it looks as if buckscript
is looking into a more streamlined way of parsing JSON
. If you are reading this from the future then be thankful you do not have to deal with encoders/decoders! Any who, after parsing the JSON
and making it into a record
I send it back to my reducer
with the action
type of NextQuote
.
self.ReasonReact.send(NextQuote(result));
It is an action
type which takes a state
type.
type action = NextQuote(state) | NoOp;
Inside of my reducer
, I use the shorthand way of updating state.
| NextQuote(result) => ReasonReact.Update(result);
It’s the same as writing:
ReasonReact.Update({ quote: result.quote, author: result.author });
There is a quirk, however, and that is this line:
self.ReasonReact.send(NextQuote(result));
You have to use self.ReasonReact.send
and not self.send
because otherwise, you get an unbound
field error. It’s one example of the weirdness of reason-react
but it’s not nearly as bad as undefined is not a function
so deal with it. Also, notice that I am passing self
in from where the fetchQuote
is called in didMount
.
didMount: (self) => {
fetchQuote(self);
ReasonReact.NoUpdate;
},
I knew I couldn’t get anything passed you! If all works as expected you should see a new random quote in your browser on each refresh.
Let’s add some interactivity! You will add a button that when clicked will fetch a new random quote. First, add an action type of Click
.
type action = NextQuote(state) | Click | NoOp;
Next, add a button that sends a Click
action to the reducer
.
<div className="App">
<div>(ReasonReact.stringToElement(self.state.quote)) </div>
<div>{ReasonReact.stringToElement(self.state.author)}</div>
<button onClick=(_event => self.send(Click))>{ReasonReact.stringToElement("Fetch New Quote")}</button>
</div>
Lastly, handle the Click
action in the reducer
.
| Click => ReasonReact.SideEffects(fetchQuote);
Again, this is shorthand for:
| Click => ReasonReact.SideEffects(self => fetchQuote(self));
If all went well, you should see new random quote after clicking the button!
The code in all it’s glory:
[%bs.raw {|require('./app.css')|}];
type state = { quote: string, author: string };
type action = NextQuote(state) | Click | NoOp;
let parseResponse = (json: Js.Json.t) : state =>
Json.Decode.{
quote: field("quote", string, json),
author: field("author", string, json)
};
let component = ReasonReact.reducerComponent("App");
let make = _children => {
let fetchQuote = (self) => {
Js.Promise.(
Fetch.fetch("[https://randomquoteapi.glitch.me/funny](https://randomquoteapi.glitch.me/funny)")
|> then_(Fetch.Response.text)
|> then_(text => {
let result = parseResponse(Js.Json.parseExn(text));
self.ReasonReact.send(NextQuote(result));
Js.Promise.resolve();
})
) |> ignore;
};
{
...component,
initialState: () => {
quote: "Hello, world!",
author: "Anonymous",
},
reducer: (action, _state) => {
switch action {
| NextQuote(result) => ReasonReact.Update(result);
| Click => ReasonReact.SideEffects(fetchQuote);
| NoOp => ReasonReact.NoUpdate;
}
},
didMount: (self) => {
fetchQuote(self);
ReasonReact.NoUpdate;
},
render: self =>
<div className="App">
<div>(ReasonReact.stringToElement(self.state.quote)) </div>
<div>{ReasonReact.stringToElement(self.state.author)}</div>
<button onClick=(_event => self.send(Click))>{ReasonReact.stringToElement("Fetch New Quote")}</button>
</div>
}
};
Wow, you just completed your first reason/reason-react app. Now when you get around the water cooler with your co-workers you will have a pretty cool story to tell! Maybe.
In this article, you learned how to create a random quote generator in reason
with the reason-react
UI library. Along the way you installed dependencies, utilized browser APIs, built a reason-react component with lifecycle methods, and created something completely useless 😆. I hope the code snippets above help you on your journey to learning ReasonML. And who knows, maybe it becomes the language that replaces undefined is not a function
.
https://github.com/arecvlohe/reason-react-medium-example
This article has Webmentions