This is the first part of blog post series where we will create blog cms using Hasura for GraphQL API, and serverless functions for logic and on the client we will write modern and robust code using ReasonML syntax. Let's get started.
ReasonML intro
First of all, before getting into actual code writing, let's discuss why ReasonML? Even though it's a topic for a stand-alone blog post, I will try to give you a brief overview. ReasonML gives us a fantastic type system powered by Ocaml, but as far as syntax goes, it looks pretty close to Javascript. It was invented by Jordan Walke, the guy who created React and is used in production at Facebook messenger. Recently various companies also adopted Reason and use it in production because of it's really cool paradigm: "If it compiles - it works."
This phrase is a very bold statement, but in fact, because Reason is basically a new syntax of OCaml language, it uses Hindley Milner type system so it can infer types in compile time.
What it means for us as developers?
It means that typically we don't write that many types, if at all, as we write in TypeScript for example and can trust the compiler to infer these types.
Speaking of compilation, Reason can be compiled to OCaml, which in turn can compile to various targets such as binary, ios, android etc, and also we can compile to human-readable JavaScript with the help of Bucklescript compiler. In fact that what we will do in our blog post.
What about npm and all these packages we are used to in JavaScript realm?
In fact, BuckleScript compiler gives us powerful Foreign function interface FFI that lets you use JavaScript packages, global variables, and even raw javascript in your Reason code. The only thing that you need to do is to accurately type them to get the benefits from the type system.
Btw if you want to learn more about ReasonML, I streamed 10h live coding Bootcamp on Youtube, that you can view on my channel
ReasonReact
When using Reason for our frontend development, we will use ReasonReact. There are also some community bindings for VueJs, but mainly, when developing for web we will go with ReasonReact. If you've heard about Reason and ReasonReact in the past, recently ReasonReact got a huge update making it way easier to write, so the syntax of creating Reason components now is not only super slick but looks way better than in JavaScript, which was not the case in the past. Also, with the introduction of hooks, it's way easier to create ReasonReact components and manage your state.
Getting started
In official ReasonReact docs, the advised way to create a new project is to start with bsb init
command, but let's face it. You probably want to know how to move from JavaScript and Typescript. So in our example, we will start by creating our project with create-react-app.
We will start by running the following command:
npx create-react-app reason-hasura-demo
It will create our basic React app in JavaScript, which we will now change into ReasonReact.
Installation
If it's the first time you set up ReasonML in your environment, it will be as simple as installing bs-platform.
yarn global add bs-platform
Also, configure your IDE by installing appropriate editor plugin
I use reason-vscode extension for that. I also strongly advise using "editor.formatOnSave": true,
vscode setting, because Reason has a tool called refmt
which is basically built in Prettier for Reason, so your code will be properly formatted on save.
Adding ReasonML to your project
Now it's time to add ReasonML. We will install bs-platform
and reason-react
dependencies.
yarn add bs-platform --dev --exact
yarn add reason-react --exact
And get into the configuration. For that create bsconfig.json
file with the following configuration:
{
"name": "hasura-reason-demo-app",
"reason": { "react-jsx": 3 },
"bsc-flags": ["-bs-super-errors"],
"sources": [
{
"dir": "src",
"subdirs": true
}
],
"package-specs": [
{
"module": "es6",
"in-source": true
}
],
"suffix": ".js",
"namespace": true,
"bs-dependencies": [
"reason-react"
],
"ppx-flags": [],
"refmt": 3
}
Let's also add compilation and watch scripts to our package.json
"re:build": "bsb -make-world -clean-world",
"re:watch": "bsb -make-world -clean-world -w",
If you run these scripts, what will basically happen is all .re
files in your project will be compiled to javascript alongside your .re
files.
Start configuring our root endpoint
Let's write our first reason file, by changing index.js from
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
to
Basically what I am doing here is render my App component into the dom with
And with
I import register and unregister methods from serviceWorker.js
file so I can use Javascript in Reason.
to run our project, we need to run
npm run re:watch
so our Bucklescript will build files for the first time and watch for changes whenever new files are added.
and in different tab let's just run npm start
and see our React app.
Basic styling
Styling with ReasonML can be either typed due to bs-css
which is based on emotion
or untyped. For simplicity, we will use untyped. Let's delete index.css and App.css we have from 'create-react-app', create styles.css
file and import two packages:
yarn add animate.css
yarn add tailwind --dev
now in our styles.css
file, we will import tailwind
@tailwind base;
@tailwind components;
@tailwind utilities;
and add styles build script in package.json
"rebuild-styles": "npx tailwind build ./src/styles.css -o ./src/index.css",
Writing our first component.
Let's rename our App.css file to App.re, delete all its contents, and write simple ReasonReact component.
Nice right? With ReasonML, we don't need to import or export packages, and in fact, each file is a module, so if our file name is App.re, we can simply use component in a different file.
String to element
In ReasonReact, if you want to add text in component, you do it by using ReasonReact.string
Also, I prefer the following syntax:
You will see it quite a lot in this project. This syntax is reverse-application operator or pipe operator that will give you an ability to chain functions so f(x)
is basically written as x |> f
.
Now you might say, but wait a second that will be a tedious thing to do in ReasonReact. every string needs to be wrapped with ReasonReact.string. There are various approaches to that.
A common approach is to create utils.re
file somewhere with something like
let ste = ReasonReact.string
and it will shorten our code to
Through the project, I use ReasonReact.string
with a pipe so the code will be more self-descriptive.
What we will be creating
So now when we have our ReasonReact app, it's time to see what we will be creating in this section:
This app will be a simple blog, which will use GraphQL API, auto-generated by Hasura, will use subscriptions and ReasonReact.
Separate app to components
We will separate apps to components such as Header
, PostsList
, Post
AddPostsForm
and Modal
.
Header
Header will be used for top navigation bar as well as for rendering "Add New Post" button on the top right corner, and when clicking on it, it will open a Modal window with our AddPostsForm
. Header
will get openModal
and isModalOpened
props and will be just a presentational component.
We will also use javascript require
to embed an SVG logo in the header.
Header button will stop propagation when clicked using ReactEvent.Synthetic
ReasonReact wrapper for React synthetic events and will call openModal
prop passed as labeled argument (all props are passed as labeled arguments in ReasonReact).
Modal
Modal
component will also be a simple and presentational component
For modal functionality in our App.re
file, we will use useReducer
React hook wrapped by Reason like so:
Notice that our useReducer
uses pattern matching to pattern match on action
variant. If we will, for example, forget Close
action, the project won't compile and give us an error in the editor.
PostsList, Post
Both PostsList and Post will be just presentational components with dummy data.
AddPostForm
Here we will use React setState
hook to make our form controlled. That will be also pretty straightforward:
onChange
event will look a bit different in Reason but that mostly because of it's type safe nature:
<input onChange={e => e->ReactEvent.Form.target##value |> setCoverImage
}/>
Adding GraphQL Backend using Hasura
Now it's time to set GraphQL backend for our ReasonReact app. We will do that with Hasura.
In a nutshell, Hasura auto-generates GraphQL API on top of new or existing Postgres database. You can read more about Hasura in the following blog post blog post or follow Hasura on Youtube [channel](https://www.youtube.com/c/hasurahq.
We will head to hasura.io and click on Docker image to go to the doc section explaining how to set Hasura up on docker.
We will also install Hasura cli and run hasura init
to create a folder with migrations for everything that we do in the console.
Once we have Hasura console running, let's set up our posts table:
and users table:
We will need to connect our posts and users by going back to posts table -> Modify and set a Foreign key to users table:
We will also need to set relationships between posts and users so user object will appear in auto-generated GraphQL API.
Let's head to the console now and create first dummy user:
mutation {
insert_users(objects: {id: "first-user-with-dummy-id", name: "Test user"}) {
affected_rows
}
}
Let's now try to insert a new post:
mutation {
insert_posts(objects: {user_id: "first-user-with-dummy-id", title: "New Post", content: "Lorem ipsum - test post", cover_img: "https://images.unsplash.com/photo-1555397430-57791c75748a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"}) {
affected_rows
}
}
If we query our posts now will get all the data that we need for our client:
query getPosts{
posts {
title
cover_img
content
created_at
user {
name
avatar_url
}
}
}
Adding GraphQL to our app
Let's install a bunch of dependencies to add GraphQL to our ReasonReact app and start getting blog posts in real-time.
yarn add @glennsl/bs-json apollo-boost apollo-link-ws graphql react-apollo reason-apollo subscriptions-transport-ws
When we work with Reason, we want to run an introspection query to our endpoint so we will get our graphql schema introspection data as json. It will be used to give us graphql queries completion and type checking in the editor later on, which is pretty cool and best experience ever.
yarn send-introspection-query http://localhost:8080/v1/graphql
We also need to add bs-dependencies
to our bsconfig.json
"bs-dependencies": [
"reason-react",
"reason-apollo",
"@glennsl/bs-json"
],
"ppx-flags": ["graphql_ppx/ppx"]
We've added graphql_ppx
ppx flag here - that will allow us to write GraphQL syntax in ReasonML later on.
Now let's create a new ApolloClient.re
file and set our basic ApolloClient
Adding queries and mutations
Queries
Let's head to our PostsList.re
component and add the same query we ran previously in Hasura graphiql:
Now we can use GetPostsQuery
component with render prop to load our posts. But before that, I want to receive my GraphQL API result typed, so I want to convert it to Records.
It's as simple as adding types in PostTypes.re
file
open PostTypes
The final version of PostsList
component will look as following:
Mutations
To add mutation to our AddPostForm
, we start in the same way as with queries:
The change will be in the render prop. We will use the following function to create variables object:
let addNewPostMutation = PostMutation.make(~title, ~content, ~sanitize, ~coverImg, ());
to execute mutation itself, we simply need to run
mutation(
~variables=addNewPostMutation##variables,
~refetchQueries=[|"getPosts"|],
(),
) |> ignore;
The final code will look like this:
Adding Subscriptions
To add subscriptions we will need to make changes to our ApolloClient.re
. Remember we don't need to import anything in Reason, so we simply start writing.
Let's add webSocketLink
and create a link function that will use ApolloLinks.split
to target WebSockets, when we will use subscriptions or httpLink
if we will use queries and mutations. The final ApolloClient version will look like this:
Now to change from query to subscription, we need to change word query
to subscription
in graphql syntax and use ReasonApollo.CreateSubscription
instead of ReasonApollo.CreateQuery
Summary and what's next
In this blog post, we've created a real-time client and backend using Hasura, but we haven't talked about Serverless yet. Serverless business logic is something we will look into in the next blog post. Meanwhile, enjoy the read and start using ReasonML.
You can check out the code here:
https://github.com/vnovick/reason-demo-apps/tree/master/reason-hasura-demo and follow me on Twitter @VladimirNovick for updates.
Top comments (7)
Looks like you don't have the user field set up on posts type. You have user_id so how would you return the name and avatar from the
users
table?this query doesnt work if i have the same set up as you:
Solution is tracking the foreign keys on your tables. See:docs.hasura.io/1.0/graphql/manual/...
Not exactly. an easier solution is what I wrote. You need to add relationships in relationships tab. I added a screenshot to clarify
Nice article!
One small thing though about this part
The result already comes typed but as a bound object (
{ . "etc": int}
) so saying "to receive the result typed" can be a bit confusing for beginners IMOI would always prefer Reason over typescript because you actually don’t need to type lots of things because compiler will infer types, but you have to be aware of the fact that Reason only looks like JavaScript. It’s way more powerful but you need to understand functional programming constructs such as pattern matching, variants and more. I suggest checking my YouTube channel for 10h ReasonML bootcamp that will cover the basics and even some advanced parts of ReasonML. And soon there will be more content on ReasonReact so stay tuned.
It's "Hindley Milner", not "Hindler Miller" type system.
It was 4 days online bootcamp 3+ h every day.