This is the last article of the series. We've built a few things with ReasonReact and now it's time to share my opinion about using Reason to create React applications.
Though, my opinion shouldn't really matter if you're also evaluating ReasonML. That's why I will share an approach that should help you decide whether to use something in production or not.
We will also see 5 tips that I learned while creating this series and that are very useful when building applications with ReasonReact.
Type coverage vs development speed
Type coverage
Ensuring a good type coverage at compile time matters because it makes our code more reliable. A bug happens when the application behaves differently from the way we intended it to behave. Type coverage forces us to be very explicit about that behavior at compile-time, which is also at "code-time" (the time you're implementing it). It's true, not all bugs are related to type. However, the more explicit we are about typing values, the more we can delegate the work of checking for bugs (testing) to the compiler itself.
A side effect of having your code statically typed is enhancing its readability. Code editors and syntax plugins can use the static type information provided by the compiler and give you hints about the code you're reading. The bigger the codebase, the more you actually appreciate that.
Development speed
The speed at which we deliver features is definitely a metric of our efficiency that shouldn't be ignored. In some contexts, it's even the first priority.
Development speed also matters because it's an important factor in the developer's experience. When a tool makes it easy to implement something fast, it's often more accessible to people and also more adopted. This is simply because most of us enjoy the results of what we build, and want to get to them as fast as possible.
So, how to decide?
When picking a tool that you will use every day to build things, it's important that you consider both type coverage and development speed.
Ideally, we would have this:
Type coverage: โโโโโโโโโโ 100%
Development speed: โโโโโโโโโโ 100%
Unfortunately, this is unrealistic.
JavaScript is amazing when it comes to dev speed. The language is super dynamic and this can be used to achieve things fast with just a few lines of code:
Here's a one-liner concatenation function:
let concat = (a, b) => a + b;
// concatenate strings
concat("Hello ", "World"); // output: "Hello World"
// concatenate strings with numbers
concat("hello", 3); // output: "Hello 3
However, JavaScript also doesn't provide any of the predictability & readability benefits we get with static type coverage.
My verdict
I started the series knowing already that ReasonML is for sure around a ๐ฏ score when it comes to type coverage.
Though, my past experience with the library made me very skeptical about the development speed. This was confirmed when I faced certain challenges like:
- React Context API.
- Async requests.
- Deserializing JSON.
However, the new syntax of ReasonReact made the development speed jump to a really high score. We're definitely not at JavaScript's dev speed score, but we're not far either. In fact, the issues I mentioned will not block you when creating applications in production. This is only possible thanks to ReasonML's flexibility and community.
This is great because we have a tool to build React application that provides a very powerful type of coverage without hurting the development speed.
In the next session, I put some tips provided by the Reason community to solve those issues.
ReasonReact tips
Tip #1: React Context
To create & use a React Context, we have to wrap the Context provider in a custom component:
/* MyContextProvider.re */
let context = React.createContext(() => ());
let makeProps = (~value, ~children, ()) => {
"value": value,
"children": children,
};
let make = React.Context.provider(context);
We can then use the created Context provider as follows:
[@react.component]
let make = (~children) => {
<MyContextProvider value="foo">
children
</MyContextProvider>
}
module ChildConsumer = {
[@react.component]
let make = (~children) => {
let contextValue = React.useContext(MyContextProvider.context);
};
Tip #2: requiring CSS
BuckleScript provides ways for requiring a JavaScript module without sacrificing type safety. However, when we require a CSS file, we don't really need any typing. Therefore, we can directly use BuckleScript's syntax for embedding raw JavaScript and write a normal JavaScript require statement:
[%raw {|require('path/to/myfile.css')|}];
Tip #3: using JavaScript React components ๐คฏ
Here's an example on how to consume an existing JavaScript React component, without hurting type safety:
[@bs.module "path/to/Button.js"] [@react.component]
external make: (
~children: React.element,
~variant: string,
~color: string,
~onClick: ReactEvent.Form.t => unit
) => React.element = "default";
Using SVGR
SVGR is a great tool that lets you automatically transform SVG into React components.
You can use the previous tip to automatically and safely import SVG components as React components through SVGR:
[@bs.module "./times.svg"] [@react.component]
external make: (~height: string) => React.element = "default";
Make sure to install the corresponding Webpack loader and add the necessary Webpack configuration.
Tip #4: performing Fetch network requests
To perform network requests from a React application, we need to use Fetch.
Here's an example on how you can make your own wrapper on top of Fetch to make POST requests:
let post = (url, payload) => {
let stringifiedPayload = payload |> Js.Json.object_ |> Js.Json.stringify;
Js.Promise.(
Fetch.fetchWithInit(
url,
Fetch.RequestInit.make(
~method_=Post,
~body=Fetch.BodyInit.make(stringifiedPayload),
~headers=Fetch.HeadersInit.make({"Content-Type":
"application/json"}),
(),
),
)
|> then_(Fetch.Response.json)
);
};
You can adjust this wrapper for other types of requests.
Tip #5: Handling JSON
Reason still doesn't have proper built-in JSON handling. In Part 2 of the series, I managed to deserialize a JSON response without using any third-party library:
/* src/Request.re */
exception PostError(string);
let post = (url, payload) => {
let stringifiedPayload = payload |> Js.Json.object_ |> Js.Json.stringify;
Js.Promise.(
Fetch.fetchWithInit(
url,
Fetch.RequestInit.make(
~method_=Post,
~body=Fetch.BodyInit.make(stringifiedPayload),
~headers=Fetch.HeadersInit.make({"Content-Type": "application/json"}),
(),
),
)
|> then_(Fetch.Response.json)
|> then_(response =>
switch (Js.Json.decodeObject(response)) {
| Some(decodedRes) =>
switch (Js.Dict.get(decodedRes, "error")) {
| Some(error) =>
switch (Js.Json.decodeObject(error)) {
| Some(decodedErr) =>
switch (Js.Dict.get(decodedErr, "message")) {
| Some(errorMessage) =>
switch (Js.Json.decodeString(errorMessage)) {
| Some(decodedErrorMessage) =>
reject(PostError(decodedErrorMessage))
| None => reject(PostError("POST_ERROR"))
}
| None => resolve(decodedRes)
}
| None => resolve(decodedRes)
}
| None => resolve(decodedRes)
}
| None => resolve(Js.Dict.empty())
}
)
);
};
Though, I wasn't satisfied with the solution since it resulted in a huge pattern-matching hell.
Since then, and with the help of the community, I found some nice alternatives using thrid-party libraries.
bs-json
Using bs-json, you can achieve the same result in a much concise way. The goal is to use bs-json to convert our JSON into records.
We first declare our record types. In our case, we needed to handle the response JSON object, which has optionally an error JSON object. We can do it as follows:
type error = {message: string};
type response = {
error: option(error),
idToken: string,
};
We can then create functions to decode the JSON objects (response & error):
module Decode = {
let error = json => Json.Decode.{message: json |> field("message", string)};
let response = json =>
Json.Decode.{
error: json |> field("error", optional(error)),
idToken: json |> field("idToken", string),
};
};
Finally, we can easily decode the JSON we receive using our decoders:
|> then_(json => {
let response = Decode.response(json);
switch (response.error) {
| Some(err) => reject(PostError(err.message))
| None => resolve(response)
};
})
ppx_decco
Another elegant way to achieve parsing JSON is to use the ppx_decco module.
We first declare our Records and prepend them with [@decco]
decorator:
[@decco]
type error = {message: string};
[@decco]
type response = {error: option(error)};
This will create under the hood 2 functions we can use to deserialize the corresponding JSON values:
error_decode
response_decode
We can then use our declared Records & the created functions to easily decode the JSON values
|> then_(response =>
switch (response_decode(response)) {
| Belt.Result.Ok({error: Some({message})}) =>
reject(PostError(message))
| response => resolve(response)
}
)
Conclusion
This series aimed to give a realistic reflection of the ReasonML to build React applications. By building UI features that resemble the ones we would in usual production environments, we managed to grasp a good feel of both the good stuff and struggles you would face if you ever decide to use Reason in production.
It's undeniable that Reason has a powerful type system with a very strong type inference that will make you write reliable code. With this series, we also saw how the development speed of React applications using Reason is also not affected. So, Yes, Reason is definitely ready to create React applications in production!
Special thanks to the Reason community on the Forums & Discord, and especially to @yawaramin for consistently reading the articles and providing help.
Top comments (5)
Wow,
ppx_decco
is cool stuff! Thanks for the tip, great series.I very much enjoyed reading this series :-) What was especially great was the perspective of the daily, boots-on-the-ground development experience of using ReasonML.
Thank you very very very much for the series, it was super helpful !
insane
Thanks for writing this series. I enjoyed reading it and look forward to more articles from you.