How to render data in a ReScript React app using fetch, promises, and safe types.
Tags: rescript, react, tutorial, webdev
Published: 2023-09-25 20:20:53

Photo by <a href="https://unsplash.com/@jamesrkern?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">James Kern</a> on <a href="https://unsplash.com/photos/gBjf2-T1_84?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>.
  

[ReScript](url) is "Fast, Simple, Fully Typed JavaScript from the Future" that has stronger types than TypeScript, a blazing fast compiler, and only includes the good parts of JavaScript.

Let's take a look at how we can create a React app that will fetch some strings from an API and render each string in a paragraph. The strings will be random from [Bacon Ipsum](https://baconipsum.com/). We'll include a loading state, error handling, and a button that will allow the user to reload the strings.

We'll be using ReScript's [Variant](https://rescript-lang.org/docs/manual/latest/variant) types to represent the state of our data, which can be "loading", "error", or "data".

We will also be using an [opaque type](https://en.wikipedia.org/wiki/Opaque_data_type) and a [parsing library](https://github.com/DZakh/rescript-struct/blob/a6fdb4efe935b679926d08c737189d801bcc59f4/README.md) to enforce some logic around what we consider to be valid data.

## Set up
I'll be using Vite to build this out. It's focus on speed makes it a great companion to ReScript. I have a guide with details on how to set up our basic ReScript, React, Vite, and TailwindCSS app [here](https://dev.to/jderochervlk/how-to-set-up-a-react-app-using-rescript-vite-and-tailwind-for-lightning-fast-development-oa).

If you used the generator you should already have a `src/Button.res` component. If you did it manually go ahead and add these two files.
```rescript
// src/Button.res
// Styling from https://tailwind-elements.com/docs/standard/components/buttons/
let make = props =>
  <button
    {...props}
    className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"
  />

```

```rescript
// src/Button.resi
let make: JsxDOM.domProps => Jsx.element
```

Replace the contents of `src/App.res` with this:
```rescript
@react.component
let make = () => {
  <div className="p-6">
    <h1 className="text-3xl font-semibold mb-5"> {"Bacon Ipsum"->React.string} </h1>
    <Button> {"refresh data"->React.string} </Button>
  </div>
}
```

And double check that you have a `src/App.resi` file.
```rescript
@react.component
let make: unit => Jsx.element
```

Kick off the Vite dev server with `npm run dev` and if you are using VSCode with the ReScript extension it should ask you if you want to start the ReScript build server, otherwise run `npm run res:dev` in another terminal to start ReScript.

You should now be able to see a basic site with blazing fast HMR when you save a file.
 

![A title that says "bacon ipsum" with a button beneath it marked "refresh data".](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7xqxn4yakmjahazzipds.png)

## Planning out our data with types
Before we start writing any features let's plan out our data and business logic with types. If you want to learn more about how to do Domain Driven Design in a functional language check out this [talk and book](https://fsharpforfunandprofit.com/ddd/) from Scott Wlaschin. He's using F#, but ReScript is also in the ML family of languages so we are able to do the same thing here.

### What is our data?
In our application we have randomized strings we get from Bacon Ipsum. We will refer to these strings as a `bacon`. Our application will render `bacon` to the user, but we want to verify that we're not just allowing any array of strings to be rendered when we expect `bacon`. This can cause bugs later once our application scales and we have 30 developers, 2 product managers, and 100,000 lines of code.

We know that `bacon` is always external and that we have to request it from an API. This means that it could be `Loading`, have an `Error`, or have `Data`.

Let's use the strong type system of ReScript to help us define `bacon` in a way that the compiler will help us follow our own business logic and prevent bugs later.

#### Create our types
Let's start by creating a type for `bacon`. In a new file called `bacon.res` we will add a type `t` that is an alias to `array<string>`. `t` is the standard name for a module's main type. Here we have a module `Bacon` that is generated from the file `bacon.res` and the main type is `Bacon.t`.

```rescript
type t = array<string>
```

We'll make this opaque later, but for now let's keep adding types.

We will now make a variant type for `bacon` to represent the possible states of our data.

```rescript
type t = array<string>

type error = string

type bacon =
  | Loading // loading has no value
  | Error(error)
  | Data(t) // and our data contains our internal type t
```

Those are our main two types we will be working with. Let's create a `bacon.resi` file. This is ReScript's interface file and it allows us to define types for a module. Having an interface file speeds up the already fast compiler and it allows us to keep some things in our module private.

```rescript
// src/bacon.resi
type t
type error

type bacon =
  | Loading
  | Error(error)
  | Data(t)
```

Notice how it looks almost identical to our implementation file, but we didn't alias `t` to `array<string>` or `error` to `string`. This now means that you can't just make anything be `Bacon.Data` or a `Bacon.Error`.

> Note: `Bacon` is now a module in our application created from the file `bacon.res`. ReScript doesn't have imports, so to access the types and values in `bacon.res` you use `Bacon.Data`.

To test this out let's go into `App.res` and try and add the following to the bottom of the file.

```rescript
let bacon = Bacon.Data(["foo", "bar"])
```

You will see a compiler error.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/serxq3gn7illdh4uyghs.png)

To anything outside of the `Bacon` module it's unknown what the type is for `Bacon.t`. This means we just can't put any value in there. The `Bacon` module will need a way for us to create something that is `Bacon.t`.

### Smart constructor
In order to take in an `array<string>` and turn it into valid `Bacon.t` we will be using a [smart constructor](https://wiki.haskell.org/Smart_constructors) which is a function that looks at our unknown input and has a runtime validation to make sure the data is correct.

We'll be using [rescript-struct](https://github.com/DZakh/rescript-struct/blob/a6fdb4efe935b679926d08c737189d801bcc59f4/README.md) as our parsing library.

> Note: the links are for v4 which is not the latest version. v5 depends on ReScript v11 which I am not using yet. V4 and V5 have slightly different APIs, but the concept is the same.

Install it with `npm install rescript-struct@v4` and add it to `bsconfig.json`.

```json
"bs-dependencies": [
  "@rescript/react",
  "@rescript/core",
  "rescript-struct"
],
"bsc-flags": [
  "-open RescriptCore",
  "-open RescriptStruct"
],
```

We'll add this to `bacon.res`
```rescript
/**
We are creating a struct that validates that the input is what we expect it to be. This check is done at run time and the function generates types that we can use when compiling.

Check the rescript-struct docs for more details.
*/
let baconStruct = S.array(S.string())

let constructBacon = x => {
  // we use parseAnyWith because we the data coming from our fetch request will be of an unknown type 
  let val = x->S.parseAnyWith(baconStruct)
  // val will be a Result, which is either OK or an Error
  // we can map this to internal values of Bacon.Data or Bacon.Error
  switch val {
  | Ok(a) => Data(a)
  | Error(e) => Error(e->S.Error.toString)
  }
}
```

## Fetch our data and assign it to the correct type
Now that we have our types defined and a way to verify the incoming data is correct, let's go ahead and fetch some data.

We'll need to install a [ReScript library](https://github.com/TheSpyder/rescript-webapi) for DOM bindings including `fetch`.

```ssh
npm install rescript-webapi
```

Add it to `bsconfig.json`:

```json
  "bs-dependencies": [
    "@rescript/react",
    "@rescript/core",
    "rescript-struct",
    "rescript-webapi" // new
  ],
  "bsc-flags": [
    "-open RescriptCore",
    "-open RescriptStruct",
    "-open Webapi", // new
    "-open Promise" // new
  ],
```

Now we can add a fetch function to `bacon.res`:
```rescript
let url = "https://baconipsum.com/api/?type=meat-and-filler"

let fetch = () => {
  // instead of .then we pipe to the then function for the next step
  Fetch.fetch(url)->then(res =>
    // we can switch on res.ok to handle a success or failure
    switch res->Fetch.Response.ok {
    // if the response is ok we can parse the json and use constructBacon to validate that the data is correct
    | true => res->Fetch.Response.json->then(res => res->constructBacon->resolve) // every Promise in ReScript has to return another Promise 
    | false =>
      // if the response isn't ok we create an Error string with the status and statusText
      Error(
        `${res->Fetch.Response.status->Int.toString}: ${res->Fetch.Response.statusText}`,
      )->resolve
    }
  )
  // and if an unknown error occurs we use Promise.catch to hande it
  ->catch(_ => Error("Something went wrong")->resolve)
}
```

We want that function to be available outside of the `Bacon` module so let's add it to `bacon.resi`:
```rescript
type t
type error

type bacon =
  | Loading
  | Error(error)
  | Data(t)

let fetch: unit => Promise.t<bacon>
```

Nice! Hopefully you can start to see how the `.resi` file allows us to keep everything that we mean to be consumed from the outside in a clean and easy to understand place. We can do whatever we want in the implementation file, but the outside world can only use what we allow them to.

## Fetch the data on page load
Now we have all of plumbing set up for the `Bacon` module we can start adding it to our React page in `App.res`.

Create a local [state](https://rescript-lang.org/docs/react/latest/hooks-state) to track our bacon data:
```rescript
@react.component
let make = () => {
  // the default state will be Loading
  let (bacon, setBacon) = React.useState(() => Bacon.Loading) //  new

  <div className="p-6">
    <h1 className="text-3xl font-semibold mb-5"> {"Bacon Ipsum"->React.string} </h1>
    <Button> {"refresh data"->React.string} </Button>
  </div>
}
```

We'll add in a [`useEffect0`](https://rescript-lang.org/docs/react/latest/hooks-effect) to call a side effect with an empty dependency array in our main `App.res` component. This will call our `Bacon.fetch` only once when the component first mounts.

```rescript
@react.component
let make = () => {
  let (bacon, setBacon) = React.useState(() => Bacon.Loading)

  /* new */
  let handleBacon = () => Bacon.fetch()->then(t => setBacon(_ => t)->resolve)

  /* new */
  React.useEffect0(() => {
    let _ = handleBacon() // we assign this to _ since we aren't returning it
    None // we have nothing to clean up so we return None
  })

  <div className="p-6">
    <h1 className="text-3xl font-semibold mb-5"> {"Bacon Ipsum"->React.string} </h1>
    <Button> {"refresh data"->React.string} </Button>
  </div>
}
```

Let's add in a quick console log to make sure we are fetching the data.

```rescript
@react.component
let make = () => {
  let (bacon, setBacon) = React.useState(() => Bacon.Loading)

  let handleBacon = () => Bacon.fetch()->then(t => setBacon(_ => t)->resolve)

  React.useEffect0(() => {
    let _ = handleBacon()
    None
  })

  /* new */
  React.useEffect1(() => {
    Console.log(bacon)
    None
  }, [bacon])

  <div className="p-6">
    <h1 className="text-3xl font-semibold mb-5"> {"Bacon Ipsum"->React.string} </h1>
    <Button> {"refresh data"->React.string} </Button>
  </div>
}
```

We can see 3 console logs happening!

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jruq5uf3tohrts0of0qd.png)

Once for the blank loading state, and two times from our `useEffect` hook in React Strict mode.

Let's go ahead and render the data with [pattern matching](https://rescript-lang.org/docs/manual/latest/pattern-matching-destructuring#sidebar):

Let's add a render function to `bacon.res` to handle each possible state.

```rescript
let render = bacon =>
  switch bacon {
  | Loading => <p className="my-3"> {React.string("loading...")} </p>
  | Error(err) => <p className="my-3"> {React.string(err)} </p>
  | Data(data) => data->Array.map(text => <p className="my-3"> {React.string(text)} </p>)->React.array
  }
```

Add that function to our `bacon.resi` file:

```rescript
type t
type error

type bacon =
  | Loading
  | Error(error)
  | Data(t)

let fetch: unit => Promise.t<bacon>

let render: bacon => React.element
```

And we can render it now in our app:
```rescript
// src/App.res
@react.component
let make = () => {
  let (bacon, setBacon) = React.useState(() => Bacon.Loading)

  let handleBacon = () => Bacon.fetch()->then(t => setBacon(_ => t)->resolve)

  React.useEffect0(() => {
    let _ = handleBacon()
    None
  })

  <div className="p-6">
    <h1 className="text-3xl font-semibold mb-5"> {"Bacon Ipsum"->React.string} </h1>
    <Button> {"refresh data"->React.string} </Button>
    // new
    {bacon->Bacon.render}
  </div>
}
```

Finally let's attach the `handleBacon` function to the `onClick` of our `Button`.

```rescript
<Button
  onClick={_ => {
    let _ = handleBacon()
  }}>
  {"refresh data"->React.string}
</Button>
```

`App.res` will now look like this:

```rescript
// src/App.res
@react.component
let make = () => {
  let (bacon, setBacon) = React.useState(() => Bacon.Loading)

  let handleBacon = () => Bacon.fetch()->then(t => setBacon(_ => t)->resolve)

  React.useEffect0(() => {
    let _ = handleBacon()
    None
  })

  <div className="p-6">
    <h1 className="text-3xl font-semibold mb-5"> {"Bacon Ipsum"->React.string} </h1>
<Button
  onClick={_ => {
    let _ = handleBacon()
  }}>
  {"refresh data"->React.string}
</Button>
    {bacon->Bacon.render}
  </div>
}
```

## Wrapping up
We now have a simple application that fetches data from an external source, verifies that data is the shape we expect, and with types can represent the possible states of our data.

### Why do we need these opaque and variant types?
In our example `bacon` is means more than just a bunch of strings. `bacon` is a list of strings that comes from a certain API. There is no other way to create `bacon`. This is incredibly powerful because we can now rely on the compiler and type system to know the source of this list of strings. We have given extra meaning to the data. 

In 6 months or a year if another developer has to work on this code, or worse we have to come back to it and remember what we did, we have set up a system that enforces data flow and will help us catch bugs.

Here are some features we will have to add to our app over the next few months. As we add these features our team will be hiring 5 new developers to help us implement all of these. I'll let you imagine how this would go in a traditional JavaScript application compared to our 

- Add a new page and api for `eggs`
- Log anytime a user sees `bacon`
- Show a special pop up to users who are seeing `eggs`
- Capitalize every first word in `bacon`
- Add a new page and api for `sausage`
- Show a log in prompt for users who are seeing `sausage`
- `sausage` now has two api responses depending on if the user is logged in or not
- if a logged in user sees `bacon` we want to send an email to that user
- business is going great! We need to add pages and apis for `fruit`, `juice`, and `coffee`!

And so on.

In our ReScript app we have strong guarantees that we can't accidentally use data with functions that might have unintended side effects. We don't want to log the wrong thing to our database, or show a popup on the wrong page. Sure, we can do all of this without types, but eventually something will be missed or forgotten.

Imagine that we have a dozen functions that do something with `bacon` and we have to add a new variant type for `AuthenticatedData`:

```rescript
type bacon =
  | Loading
  | Error(error)
  | Data(t)
  | AuthenticatedData(t)
```

In any function that does pattern matching with `bacon` we will get a warning from the compiler:
```
You forgot to handle a possible case here, for example: AuthenticatedData(_)
```
We can now go through each function and update the switch to handle the new case. We can even configure the ReScript compiler to error instead of warn for this to prevent us from shipping it to production.

## Codesandbox
[Here's](https://codesandbox.io/p/sandbox/dazzling-rain-hdvq9k?welcome=true) the app running in Codesandbox if you want to take a look around.

## Questions?
Feel free to ask anything in the comments!

## My other ReScript articles
{% embed https://dev.to/jderochervlk/how-to-set-up-a-react-app-using-rescript-vite-and-tailwind-for-lightning-fast-development-oa %}
{% embed https://dev.to/jderochervlk/how-to-handle-routing-in-a-rescript-react-app-using-pattern-matching-2ban %}