First off, this tutorial assumes that you have some knowledge of how React works. If youâre just getting started to React, Iâd highly recommend reading through The Road to learn React before trying to dive into ReasonReact. Itâs really important to have at least a basic foundational understanding of React. After all, ReasonReact is an abstraction on top of React.
Kinda reminds me of this Kyle Simpson quote from You Donât Know JS: Async & Performance : Itâs impossible to effectively use any abstraction if you donât understand what itâs abstracting, and why.
The other abstraction thatâs important to know about is Reason itself, since ReasonReact is React in Reason . If youâre just getting started with Reason Iâd recommend having the Reason docs up as you read this article, just in case you need any refreshers on the syntax or language mechanics. I also have an introductory article to Reason that may be a good read if youâre just starting out with Reason and want to know what all the hype is about.
In addition, thereâs one disclaimer Iâd like to add. Weâll definitely see some ârough edgesâ and not-quite-finished language features as we go through this tutorial. This is largely because Reason is a newer community, even though itâs based on OCamlâs more mature community. Itâs important to remember that the teams behind Reason and ReasonReact are aware of the pain points and awkwardness around certain parts of the language, and are working hard to fix them. While certain parts (like async/await or CSS) arenât fully solved yet, thereâs a lot of really smart people working to solve them right now.
For this tutorial, weâll be building a Github Search app. This app will have a search bar at the top that takes any topic your heart desires. Then, when the search entry is submitted, weâll query the GitHub REST API and display a list of repositories matching that topic.
Hereâs a live link to the app if you want to poke around a little bit.
I find it helpful to build an app of this size whenever Iâm learning something newâin fact, I often use this exact app requirements to learn a new technology or framework. Solving a familiar problem is a good way to get a feel for an unfamiliar technology. Similar to the way people make to-do lists or Hacker News clones, a GitHub search app is just complex enough that weâll have to do things like state-management and API calls, yet simple enough to build in a single tutorial.
If youâre looking to look through the source code you can check out the repo here . To code along check out the getting-started branch. This will only contain the boilerplate to get a âhello worldâ on the screen, and then weâll fill in the rest of the app from there.
That should start a simple dev server at http://localhost:8000 with a very plain âHello Worldâ on the screen.
Letâs start by making a stateless component. Weâre gonna create one of the cards that contain the list results. Weâll add a new file to the src directory named Card.re .
Youâll notice that while the dev server is running adding our src/Card.re file will generate a Card.bs.js file right next to it. This is the compiled Reason code for our src/Card.re file. The BuckleScript build system generates a JavaScript file per Reason file; this makes it easy to introduce Reason into a JavaScript codebase.
The first thing we have to do for a ReasonReact component is create a component âtemplateâ. You can think of this as the React.Component that you would extend off of when creating a class component in JavaScript. ReasonReact doesnât use classes, so this template is a record (similar to a JS object, but immutable) that we can override with our custom component code.
To make our component template weâll call the ReasonReact.statelessComponent function. Passing "Card" as the argument gives our component itsâ name.
To actually create a component using our template we need to define a function with the name of make . This make function takes our componentâs props as labelled arguments (a labelled argument in Reason is an argument starting with a ~ ).
For our use cases, weâll have our Card component use name , description and an href props. This will give us enough to see what repos match our search as well as include links to them.
In addition, the make function has to take a children argument as itsâ last argument, even if the component doesnât do anything with itsâ children. This is to preserve the type-safety of ReasonReact so that it can do all of itsâ compile-time magic later on. If you donât plan on using the children prop, just add an underscore to the beginning ( _children ) to tell the compiler you didnât plan on using the variable.
Now that weâve got an empty make function, what should it return? ReasonReact expects make to return a record with a bunch of internal keys and the componentâs lifecycle hooks. Fortunately, we can use that template we made earlier. Letâs spread the template into our make functionâs return value.
Itâs also worth noting that if youâre coming from JavaScript land, curly braces after an arrow in Reason donât behave like curly braces in JavaScript. In Reason, the curly braces after the arrow mean weâre actually returning a record, as opposed to just starting a new multiline function body.
Now that weâve spread all of our template into make , letâs add our own custom render function.
Letâs take a quick look at the JSX first. Itâs built-in to Reason at the language level but you might notice a few differences from the JSX youâre used to.
First off, Reason JSX supports punning âwhen the prop name matches the variable thatâs being passed as itsâ value, you can just write the prop once instead of twice. So since we already a variable named href we donât need to write make={make} when applying it to our <a> tag. Instead, we can just do <a href> .
In addition, Reason JSX doesnât require prop assignments to be inside curly braces. So instead of href={link} you could do href=link and it will work exactly the same. If you prefer the curly braces go ahead, both are valid syntax.
However, there is one big difference from ReasonReact has compared to React JSX. Whatâs all this {ReasonReact.string(description) business? Once again, this has to do with type safety and being in a compiled language. Since each componentâs render has to return a React element ReasonReact provides a series of utility functions to convert strings, arrays, and null into the correct type for ReasonReact to use it correctly. It does feel a little awkward at first, especially coming from regular JSX. However, I will say the awkwardness does wear off a little bit, especially when you realize the absolute type safety Reason is adding to your app.
If youâre still annoyed by having to write {ReasonReact.string(description) , you can create a <Str string={description} /> component that just wraps ReasonReact.string() . This might feel a little less awkward and boils down to essentially the same thing.
Weâre done! This is what our completed <Card /> component looks like.
Letâs integrate it into our app so we can see it in action. If you cloned the repo go check out the src/App.re file. Youâll notice itâs pretty bare right now.
Letâs replace the âHello world!â text with our <Card /> component. Weâll have to add some fake props since we havenât added real data just yet (donât worry, weâll get there soon).
We also didnât need to import our <Card> because every file in Reason is automatically a module in the global namespace. This takes a little getting used to, but Iâve found that the Reason/OCaml module system can be quite elegant. Not having to explicitly define an import path makes it easy to move files around or update folder structure later on. It also makes it easier to just pull in the modules you need without interrupting your workflow when youâre in the middle of a project.
When we check out the page we can see that our card is indeed on the page correctly, although itâs a little bare.
Letâs add some styles to our <Card /> before we go any further. Any real app will have styles, so it wouldnât feel right if I skipped over some of ReasonReactâs styling approaches.
Thereâs a few methods of styling in Reason, although I have yet to see a single method âwinâ as âthe official wayâ to do styling just yet.
As a simple styling solution thereâs always inline styles. ReasonReact includes a way to create the same style object that React uses under the hood. This is what an inline style declaration would look like.
ReactDOMRe.Style.make is a function that takes a number of optional labelled arguments. Each argument directly maps to a CSS property. The last argument to ReactDOMRe.Style.make is a little different, itâs a value called unit () . Believe it or not, this is a pretty common convention in the Reason/OCaml community for managing large amounts of labelled optional arguments. That said, it looks a little strange if youâve never seen it before.
Basically, the reason that the final argument has to be unit is to signal when to stop currying the function. Since Reason function arguments are automatically curried , calling ReactDOMRe.Style.make(~padding="1rem"); returns a new function that we could pass more CSS properties into. Using functions like this lets us progressively apply values into our function throughout our application rather than all at once.
If we wanted to pull our styles outside of render , Iâve found it helpful to use a local module . This can help add some readability to our render if styles are getting a little long.
Another commonly-used community solution to styling is bs-css , which is a typed wrapper around emotion . If we wanted to use bs-css first we would need to install it.
And then we will need to add bs-css to the "bs-dependencies" field in our bsconfig.json file (if you cloned the sample repo it will be right there alongside package.json ).
Now we can go convert our styles to use bs-css , which will generate a string that we can use as a className . Using bs-css gives a little more type safety to our css styles, if thatâs something that youâre looking for.
Sidenoteâif your IDE is yelling about an unbound module warning after you added bs-css , try reloading it or re-opening the file. I use VSCode and I commonly get this error after installing new Reason packages. The reason (no pun intended) has to do with the IDE loading dependencies when a file is first opened and you adding dependencies after the file was opened. Chances are the compiler error will look like this: âError: Unbound module Cssâ.
That said, thereâs a lot of other ways to manage styles in ReasonReact. These are only two of commonly-used methods. Iâve personally used a custom binding to Emotion that provides a little less type safety for style rules, but feels a little closer to the tagged template literal API.
Thereâs also a really promising project for a PPX transform for CSS . You can kinda think of a PPX transform as a Babel plugin for the Reason/OCaml language. It allows the ability to use custom syntax to describe CSS. This would allow something much closer to plain olâ CSS, without sacrificing any type-checking power. I havenât fully played with it just yet, but Iâve heard good things so far.
For now, letâs make do with bs-css as a styling solution, but itâs always good to know that other options exist if bs-css isnât your cup of tea.
Now, letâs build the search form. Weâre gonna do this directly inside of src/App.re for simplicityâs sake, so weâll be converting <App /> from a stateless component to a stateful component.
ReasonReact calls itsâ stateful components reducer components . In my opinion, reducer components showcase the benefit of adding the battleproof type-safety of Reason/OCaml to our React code. Itâs easier to sacrifice type-safety when youâre writing a simple card component, but once you start adding business logic to your components that type-safety helps protect us from silly mistakes.
As we dive into reducer components I find it helpful to think of the way that Redux reducers work. Reducer components feel very similar to Redux, except that theyâre contained within the component itself instead of being connect to a global state store. If youâre unfamiliar with Redux or want a refresher on how it works, check out Taming the State in React .
The first thing that weâll need to do to turn our <App /> component into a reducer component is create a couple type declarations. The first one weâll need to create is a state type to describe what our componentâs state looks like. Letâs just add it at the very top of the src/App.re file.
The second type weâll need to make is an action type. Similar to a Redux action, this will describe the types of ways we can update our componentâs state. Weâll define the action type as a variant .
For now, weâll have two possible actions to update our componentâs state, UpdateInput and Search . UpdateInput will trigger whenever the user types into the search bar, passing the value of the input field as a value. Search will represent when the search query is actually submitted and we want to grab the search results from GitHubâs API.
Next we need to modify our component template to use a reducer component. To do that weâll need to change ReasonReact.statelessComponent("App") to ReasonReact.reducerComponent("App") . Itâs not a big change, reducerComponent takes the exact same argument as statelessComponent : the name we want to give our component.
Now weâre using the reducer component template. Weâre not quite done converting our stateless component just yet though, so donât worry if you see compiler warnings for now. For a reducer component, we do need to provide a couple extra keys to our component record in addition to render .
The first thing weâll need to add is an initialState key. This key has to be a function, and it has to return the same state type that we defined earlier.
The second thing weâll need to add is a reducer function. This works exactly the same as a Redux reducerâit takes an action and state as arguments and returns an update to the state. Technically it returns a special update type that manages the setState that you would normally do in JavaScript. However, the argument to the update type is the next state that you would like your component to have, so we can just think about the reducer as returning the updated state.
Inside of our reducer, weâll use pattern-matching to declare our state updates for each action. The pattern-matching syntax looks a little bit like a JavaScript switch statement. However, unlike a switch statement, Reasonâs pattern-matching is 100% type safe. The compiler will even warn us if we forgot to declare a state update for one of our actions.
For the UpdateInput actions weâll just pass that value along as the new input. This will make sure our input value stays in sync with whatever the user is typing. For the Search action, weâll just turn the isLoading state on. Weâll flesh this out a little more when we cover data handling.
The last thing left to do to convert our component is to modify our render function to use the state that we just added. Since this step is a little more involved, weâll make sure to do it in a few stages.
Letâs start by replacing our <Card /> with a form containing an input and a submit button. The input field will be hooked up our state.input . Donât worry about adding the event handlers just yet, weâll get there soon!
In addition to the form, weâll also render the text âLoadingâ¦â if state.isLoading flag is true . Since we donât have any state updates built yet, this wonât change yet. For now, letâs just get the elements hooked up to state correctly.
A couple things to note in this example. Since Reason doesnât come with the concept of this the way JavaScript does, weâll have to use the self argument in render to access our componentâs state. In addition to state , self contains a few functions to help with updating state, correctly binding event handlers (for functions outside of the component), stuff like that. Think of self as your workaround for this , without all of the baggage and confusion about context.
Another little âgotchaâ is the type_ attribute on the <button> tag. Since type is a keyword in Reason the Reason team has built in a workaround for variables (and props) that match keywords: just append an underscore at the end and youâre good to go.
Lastly, the loading text isnât quite as simple as the {state.isLoading && "Loading..."} that we would see in JavaScript. This comes down to the type system once againâin JavaScript we can rely on falsy expressions magically converting to null which renders as empty in React. In Reason we have to explicitly say that we want to render null using ReasonReact.null and a ternary statement in order to satisfy the compiler.
This is all cool and all, but our form isnât really going to be much use if we canât update or submit it. Letâs add a couple event handlers to make our form work as intended. For readabilityâs sake, letâs define the handlers outside of render as plain functions. We can just put them up above the make function.
The first event handler weâll add is on the input field. Weâll just take the value out of input.target.value and trigger a state update with our UpdateInput action. Letâs just define our event handler inline inside of render for now (if you would like to pull them out of render later on youâre more than welcome to, however you will need to read up on using the self.handle function to wrap your handler).
The first part ( let value = ReactEvent.Form.target(ev)##value; ) is roughly equivalent to let value = ev.target.value; in JavaScript. Itâs certainly less ergonomic than itsâ JavaScript cousin, but once again this has to do with getting the compiler to be happy. Iâve yet to find a simpler or cleaner way to do this, if you know of one let me know.
We can think of the second line of our handler ( self.send(UpdateInput(value)) ) similarly to the way we would use a Redux dispatcher . Essentially what self.send does is it makes sure that the UpdateInput action and the input value are passed into our reducer function so we can generate a new state.
Now that weâve got our input handling changes to itsâ value correctly, letâs wire up the form submission. The first thing weâll want to do is hook up a relatively small event handler to prevent the default form submission action (reloading the page) as well as firing the Search action with self.send to tell our componentâs reducer that itâs time to handle the form submission.
Weâre keeping the event handler itself fairly lean so most of our fetching & data normalization logic can go inside the reducer function. However, to allow our component to run these functions in the reducer weâll need to modify the Search part of our reducer to use ReasonReact.UpdateWithSideEffects instead of just ReasonReact.Update . This function behaves exactly as itsâ name suggests: it updates the state, and then triggers a side effect. We can do whatever we want in those side effects, so this will be perfect for allowing us to trigger an API request and add some loading state after the form is submitted. Letâs update our reducer now.
UpdateWithSideEffects allows us to pass a second argument to our state updateâa callback to be executed after the state is set (If youâre familiar with a setState callback , this works similarly). Triggering our side effects this way sis the preferred method since it keeps most of our appâs logic contained inside the reducer method. In addition, itâs a little safer as far as preparing for the future of React with async rendering.
The first thing weâve done inside of our side effect is pull our input value out of self.state.input . Weâll use this for our API query coming up.
Weâve come a long way! Weâve got an operating form that triggers our loading state and a <Card /> component for once weâve got a list of results. Now we just need to connect the dots and get the real data from GitHubâs API into our app.
Data fetching in Reason is a lot easier said than done. Actually calling the API isnât too hard, but the trickiness starts once we receive a response. Because Reason is statically typed it needs to make sure that the API response is correctly mapped into valid Reason/OCaml types. We call this process of parsing the JSON and transforming it into valid types JSON decoding .
JSON decoding can be kind of tricky. The âproperâ way to do it is to declare every single key* in the JSON that you care about. Then you try to map each key to the type you want it to be on the Reason side. If it maps, great! But if it doesnât map correctly you assume itâs bad data and throw out the entire key, potentially replacing it with a default value. This can get really verbose, but this method ensures that you handle any malformed data when it enters your app instead of letting it cause bugs later on.
Granted, you could write some external bindings and essentially tell the compiler âthis is what my JSON looks like and it will never be different than this typeâ. But rarely in the real world do our external APIs always returns exactly what we expect. Sometimes they crash or return 500 errors. Sometimes that key we expected to contain a number is actually null . Cutting corners on type bindings here might be convenient, but one of the main selling points of using a typed language like Reason is the compiler and the safety a it brings to the table.
All that said, since weâre doing this tutorial to get a flavor of what ReasonReact feels like, weâll do the full JSON decoding. Thereâs a few community libraries to make our JSON decoding and API fetching a bit easier. So before we jump into our fetching logic, lets install bs-fetch and @glennsl/bs-json . The first is a thin wrapper around the native window.fetch function, and the second will give us a bunch of utility functions to ease the decoding process.
Weâll also need to add them to the bs-dependencies field of our bsconfig.json .
Since the data fetching and JSON decoding is gonna be quite a bit of code, letâs create a local Api module inside of our src/App.re component. This will help encapsulate it and keep our code from getting too far nested. You can just put it between the let component declaration and the make function.
Next thing weâll want to do is set up a function to make the API call. Weâll use the bs-fetch module to send the request. For now, we can just convert the response to JSON and resolve the promise.
Sadly, Reason doesnât have a full-fledged async/await syntax just yet, although itâs in progress (see this PR ). So weâll have to live with regular promises in Reason until a proper async/await solution is implemented.
Letâs make sure our getResults function is actually fired when we submit the form. That way we can make sure our query is getting a response before we start writing our decoders. Weâll call Api.getResults from our reducer side effect.
If you fill out the search input and submit the form, youâll see the API request triggered in your DevTools, as well as the response in the console. That means we can start decoding our results and turning them into something that Reason can accurately use for itsâ type system.
Before we write our decoder functions, weâll need to add a type declaration for the shape that we would like our data to be. This will be the return type of our JSON decoder and weâll eventually add it to our component state. Letâs create a repository type that contains 3 keys: a name, the URL, and a short description. We can add it up above our state declaration.
Great! Now weâre finally ready to start adding the decoder function. To use all of the decoding functions inside of bs-json , weâll add open Json.Decode; at the top of our local Api module. This essentially pulls in all of the exported functions from the Json.Decode namespace into our local module. Instead of having to type Json.Decode.functionName we can just type functionName . While itâs not good to always open a module it can greatly decrease verbosity.
In the decoder function itself, weâll do a couple things. The part of the API response that we want is inside the items array. Each object in the items array contains a lot of data, but we only care about those 3 keys from our repository type. What we need to do is tell Reason to look at the items field of the JSON and turn it into a list of our repository type.
However, if any of our fields inside of the repository record isnât converted correctly, we donât want to convert the data. Because of this weâll wrap our repository decoder inside a special optional wrapper. This basically says to return an option type ), so that we can have Some(repository) or None if the conversion was invalid.
Hereâs what the decoding function actually looks like. Weâll call it decodeResults .
The last thing is to add our decoder function into our promise chain so that we actually execute it on the API results. Weâll also need to add a step to filter out any repositories that didnât convert correctly.
And thatâs it! Our JSON will now be available through the resolved promise as a valid Reason data structureâa list of repository records, to be exact. While the actual decoding function isnât too large all by itself, I found that when I was first jumping into Reason decoding JSON was extremely tricky because I wasnât familiar with it yet. Compared to JavaScript it can easily feel like a lot of verbosity just to get some data into your app. In our case it was only 3 keys per item, but imagine if you needed 20 keys, or if you had data nested further inside of objects. That said, the practice of sanitizing data when it comes into our apps is a good thing to do, and having to do this decoding step forces us to verify that the data is the way we expect it to be later on when we use it.
Speaking of using the data, weâre coming down the home stretch on our data handling. All thatâs left to do is add the data to our componentâs state. Since weâre gonna want to store it in state, weâll need to update our state type to reflect this new data.
Weâll also likely see a compiler error that we need to update our initialState function since we changed the state . Letâs just start off with an empty list.
Now we can actually update our component to store the new data in state. Letâs create a new action called UpdateResults in our action type and add another branch to the reducer to handle that action.
While we could cram all the state updates in with our API-calling code, that could easily start to get convoluted and messy. Separating the state updates into a new action will help untangle the logic there a little bit.
The only thing weâll do in our API-calling part of the reducer is trigger another action with self.send , this time telling the component to update state with our new UpdateResults action and our decoded JSON data.
Whew. Give yourself a pat on the back. Youâve successfully fetched the JSON and brought it into your componentâs state. This is why I personally like to build this GitHub search app when learning a new framework or languageâitâs simple enough you donât spend weeks on a project, but complex enough that you get a feel for more difficult things like data handling and state management. Having complex decoding steps is actually fairly common for static compile-to-JavaScript languages like Reasonâbelieve it or not Reason is less verbose at decoding JSON than some others.
The final thing to do for our component is display our repository results inside of render . Since weâve already built the stateless <Card /> component we can just hook it up to our data.
Thatâs it for our intro to ReasonReact. Although this was a simple app with barebones styling, weâve covered a ton of ground. We saw what a stateless component looks like in ReasonReact and how ReasonReact handles statefulness with reducer components. We also went through the ceremony of data fetching and normalization that comes along with bringing unsafe JSON into a type-safe world.
If youâre interested in adding Reason to a side-project or moving parts of a codebase into Reason, youâre in luck. Since Reason compiles to plain JavaScript files you can incrementally introduce ReasonReact to your codebase. This means you can skip the massive rewrite and start playing with this new technology in a non-invasive manner. Just compile your ReasonReact components down to JavaScript and import them from your JavaScript React components.
I hope that throughout this article youâve enjoyed getting a feel for ReasonReact and the value that it can bring to some logic-heavy components. Or at the very least I hope that peeking into ReasonReactâs approach to state management and data handling brought some new approaches you can bring back with you to JavaScript codebases. Reason might not be fully mature enough to go all-in on just yet but it seems like itâs got a bright future ahead of it. Lastly, if you have any ideas or if you know of better ways to set up the components we wrote today, let me knowâIâd love to hear! Feel free to follow me on Medium or check out my Twitter .