Eliminating Illegal State in ReScript

rescriptMarch 23, 2018Dotby Alex Fedoseev

Next thing I’d like to share is how ReScript helps in making illegal states unrepresentable in our apps.

Here’s the common pattern of shaping a state in JS:

type State = {|
  loading: boolean,
  data: Data | null,
  error: Error | null,
|};

What’s wrong with this? Let’s see how it can be handled in UI:

render = () =>
  <div>
    {this.state.loading && <Spinner />}
    {!this.state.loading &&
      !this.state.error &&
      <div>{this.state.data.foo}</div>
    }
    {this.state.error && <div>{this.state.error.message}</div>}
  </div>

It can be improved a bit by re-shaping this state:

type State = {|
  status: "loading" | "ready" | "error",
  data: Data | null,
  error: Error | null,
|};

render = () => {
  switch (this.state.status) {
  case "loading":
    return <Spinner />;
  case "ready":
    return <div>{this.state.data.foo}</div>;
  case "error":
    return <div>{this.state.error.message}</div>;
  default:
    throw new Error("¯\_(ツ)_/¯");
  }
}

But the main issue is still there: conditional dependencies between state properties. When loading === false (or status === "ready") it implicitly assumes that data is not null. No guarantees though. Opened doors into an illegal state.

When you use Flow or TypeScript, these tools warn you that data might be null and you have to add all these annoying checks to calm type system down:

case "ready":
  if (!this.state.data) {
    throw new Error("Uh oh no data");
  }
  return <div>{this.state.data.foo}</div>;

I look at the code above and I’m just sad.

The light

First of all, let me introduce you to the thing called variant.

type status =
  | Loading
  | Ready
  | Error

This is type similar to enum in TS except its parts are not strings (nor any other data type you familiar with from JS). These things called tags (or constructors).

Now the magic moment: every tag can hold its own payload!

type status<'data, 'error> =
  | Loading
  | Ready('data)
  | Error('error)

If you’re not there yet, here’s the code (mixing ReScript & JS just for clarity’s sake!):

type Status<'data, 'error> =
  | Loading
  | Ready('data)
  | Error('error);

type State = {status: Status};

class Foo extends React.Component {
  state = {status: Loading};

  componentDidMount = () => {
    api.getData()
      .then(data => this.setState({status: Ready(data)}))
      .catch(error => this.setState({status: Error(error)}));
  };

  render = ({state}) => (
    <Layout>
      {
        switch (state.status) {
        | Loading => <Spinner />
        | Ready(data) => <div>{data.foo}</div>
        | Error(error) => <div>{error.message}</div>
        }
      }
    </Layout>
  );
}

How beautiful is that! There’s no way to get into an illegal state in this component. Everything is type safe. Combine it with “everything is an expression” in ReScript and you can use pattern matching at any point of your JSX render tree (spot switch as a child of <Layout />).

You know what to do.

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right