ReScript records, NextJS, undefined and getStaticProps
Tags: rescript, javascript, nextjs
Published: 2021-05-15 12:46:07

[NextJS](https://nextjs.org/), a pretty solid framework for building React based websites and web-applications, offers a nice feature for generating static pages from e.g. fetched endpoint data via the `getStaticProps` API, which looks something like this in JavaScript:

```js
export default function MyComponent(props) {
  // props provided by getStaticProps
}

export async function getStaticProps(context) {
  // do your async stuff here to fetch data
  return {
    props: {}, // pass the data as props to your component
  }
}
```

There is an important restriction though: The value defined as `props` must be **JSON serializable**. JavaScript objects usually resemble JSON data by default, so oftentimes this isn't really an issue. There are still some subtle cases where confusing errors pop up, so this article will describe a typical error scenario a ReScript developer will most likely face when working with NextJS.

## The JSON Issue w/ undefined values

Let's assume we want to use the `getStaticProps` API and return some props based on a ReScript record:

```rust
// ReScript code

type recipe = {
  diet_kind: option<string>
};

let getStaticProps = (_ctx) => {
  let recipe = {
    diet_kind: None
  };
  
  //last statement is the return value
	{
	  "props": recipe
	}
};

```

As soon as you compile this code and boot up the Next development server, you will get an error similar to this:

```
Error: Error serializing `.diet_kind` returned from `getStaticProps` in "/".

ReScript: `undefined` cannot be serialized as JSON. Please use `null` or omit this value all together.
```

Let's explain what's going on here.

First of all, a ReScript record will compile to a JS object with the same object structure. The `diet_kind` defined as `option<string>` may be one of two different values, which compile to the following JS:

- `Some("My Diet Name")` will be compiled to `"My Diet Name"`
- `None` will be compiled to `undefined`

As soon as I construct a recipe value `{ diet_kind: None }`, it will be compiled to `{ diet_kind: undefined }`, which is not a well defined JSON value.

There are two solutions on how to tackle this (as already noted by the error message above), and I'd like to show how to do this specifically in ReScript.

## Use `null` instead of `undefined`

Instead of using `option`, we need to fall back to the JS specific interop type `Js.Nullable.t`:

```rust
type recipe = {
  diet_kind: Js.Nullable.t(string)
};

let myRecipe = {
  diet_kind: Js.Nullable.null
};
```

This will compile `myRecipe` into `{ diet_kind: null }` in JS, which now is valid JSON. This solution is functional but somewhat impractical. Pure ReScript code doesn't have any `null` values and uses `options` everywhere for expressing existing / non-existing values. So every time we want to use a Next based API, we'd need to map and convert those `options` to nullable types back and forth.

For example, on the component side, we'd now need to handle the `recipe` value like this:

```rust
type props = recipe;

// Interop React component without react.component ppx
let default = (props: props): React.element => {
	 // Convert diet_kind back to option
   let diet_kind = Js.Nullable.toOption(props.diet_kind);
   
   switch(diet_kind) {
     | Some(kind) => Js.log2("here is the kind: ", kind)
     | None => Js.log("No kind found")
   };
   
   <div/>;
};
```

This doesn't really feel like a very accessible approach, so I was curious if there are other ways to do it. The alternative is a little bit more hacky and unsafe though.

## Omit all `undefined` values all together

The other approach would be to completely strip every object attribute that is set to `undefined`. This is actually hard to do for pure ReScript code, since the JS object representation of a record will always maintain the full structure with all its attributes attached, even if they are undefined.

That's why we'll need to go the JS interoperability route and use unsafe code to strip away those values. `getStaticProps` is only used during build time, so I think it's okay to use the `JSON.stringify` / `JSON.parse` functions to do the dirty work for us. As a reference, that's what we want to do in JS:

```js
const v = { a: undefined, b: "test" };
const json = JSON.parse(JSON.stringify(v));

// json = { b: "test" }
```

As you can see, `JSON.stringify` drops all the values that are not part of the JSON definition. It's also a quite generic approach, since it strips all non-JSON values recursively, and can be applied to any json data `object` or `array`.

**Before proceeding to the interesting part, here's a word of caution:** In ReScript it's often a trade-off between type-safety and convenience. The solutions proposed here are **unsafe by design** for practical reasons. If you are striving for a 100% type-safe codebase, this approach is not for you.

---------------------UNSAFE PART FROM HERE ---------------------

### Somewhat unsafe `undefined` stripping for ReScript records

Let's get back to our recipe example with `option` values. We will start out putting everything in a `Recipe` module so we can easily add related functions for our recipe data type `t`:

```rust
module Recipe = {
  type t = {
	  title: string,
	  diet_kind: option<string>
	};
}
```

Now let's define an independent function `stripUndefined` for the stringify / parse logic:

```rust
let stripUndefined = (json: Js.Json.t): Js.Json.t => {
  open Js.Json
  stringify(json)->parseExn
};
```

Since `stripUndefined` defines `Js.Json.t` values as its input and output, we need to add **very unsafe external functions** to our `Recipe` module:

```rust
module Recipe = {
  type t = {
    title: string,
	  diet_kind: option<string>,
	};
	external fromJson: Js.Json.t => t = "%identity";
	external toJson: t => Js.Json.t = "%identity";
}

let stripUndefined = (json: Js.Json.t): Js.Json.t => {
  Js.Json.(stringify(json)->parseExn);
};
```

**Note:** As mentioned before, a ReScript record is represented as a JS object during runtime (so basically a JSON structure, if we only use JSON related values inside). With the `fromJson` and `toJson` identity externals, we are _lying_ to the compiler that our type `Recipe.t` is equivalent to a `Js.Json.t`, so this is completely unsafe and it should be made sure that your `type t` really is handling values that are Json compliant (except undefined values of course). That means you should only use values such as `string`, `float`, `object`, `array`, and of course `option` (but no other values such as functions, otherwise they will be stripped as well).

Now let's combine all these things together to see it in action:

```rust
let myRecipe = {
  Recipe.title: "My Recipe",
  diet_kind: None
};

let default = (props: recipe) => {
  // No convertion from Js.Nullable needed anymore!
  switch(diet_kind) {
   | Some(kind) => Js.log2("here is the kind: ", kind)
   | None => Js.log("No kind found")
  };
	<div/>;
};

// Simulating the getStaticProps Api without any types here
let getStaticProps = (_ctx) => {
	open Recipe;
	// Clear our recipe from any undefined values before returning
	let props = toJson(myRecipe)->stripUndefined->fromJson;
	{
	  "props": props
	}
};
```

That's it! After compiling and rerunning the code, the error is gone and our app works as expected.

**Note:** If you are handling an `array(recipe)`, you can apply the same technique to the whole array in an `Js.Json.t` as well, since stringify / parse can act on json objects and json arrays.

It's also noteworthy that the last `fromJson` call is not needed when we are not enforcing any type shapes for the `props` value. In my typical ReScript / Next projects (see my [rescript-nextjs-template](https://github.com/ryyppy/rescript-nextjs-template) ReScript template) I enforce the props type across `getStaticProps` and my React components.

(As long as you are making sure that `Recipe.t` contains JSON compliant values, it's also perfectly safe to continue using the modified record returned by `toJson(myRecipe)->stripUndefined->fromJson` in ReScript code, since all runtime operations on an `option` value will continue to work as intended.)


## Conclusion

We highlighted the problems with NextJS' `getStaticProps` props value limitations and why this is relevant for writing idiomatic ReScript code that interops well within the framework.

We showed how we can tackle the JSON serialization problem, either by converting `option` to `Nullable.t`, or do unsafe `undefined` value stripping using `JSON.stringify` / `JSON.parse`. We also dabbled a little bit into the world of compile type / runtime value representation of ReScript records.

If you are interested in more ReSCript related content, make sure to [follow me on Twitter](https://twitter.com/ryyppy) and stay tuned for more practical insights!